一、添加进程
一、进程状态确认
首先,系统需要确认进程的状态。在Linux中,进程有多种状态,如TASK_RUNNING(运行中)、TASK_INTERRUPTIBLE(可中断等待)、TASK_UNINTERRUPTIBLE(不可中断等待)等。只有状态为TASK_RUNNING的进程才会被添加到runqueue中。
二、选择目标runqueue
-
负载均衡考虑:为了系统负载的均衡,Linux内核在选择将进程添加到哪个runqueue时会考虑当前各个CPU的负载情况。如果某个CPU的负载较轻,那么新进程更有可能被添加到该CPU的runqueue中。
-
优先级匹配:除了负载均衡外,内核还会考虑进程的优先级。每个runqueue都维护了多个优先级队列,内核会根据进程的优先级将其添加到相应的队列中。
三、添加进程至活动队列
-
分配task_struct:每个进程在内核中都有一个对应的task_struct结构体,它包含了进程的各种信息,如状态、优先级、调度信息等。在将进程添加到runqueue之前,系统会为其分配一个task_struct结构体。
-
链接至活动队列:进程通过其task_struct结构体中的run_list成员链接到runqueue的活动队列中。活动队列维护了正在运行且时间片还没有结束的进程。
四、更新调度信息
-
更新bitmap:runqueue中的每个优先级队列都有一个对应的bitmap数组,用于快速判断该队列是否为空。当进程被添加到某个优先级队列时,系统会更新对应的bitmap数组,以反映队列的非空状态。
-
调整调度算法:Linux内核采用了多种调度算法,如O(1)调度算法等。在将进程添加到runqueue后,系统会根据当前的调度算法和进程的优先级等信息,调整调度策略,以确保系统的高效运行。
五、等待调度
-
等待CPU时间片:一旦进程被添加到runqueue的活动队列中,它就会等待CPU的调度。当CPU空闲或当前运行的进程时间片耗尽时,调度器会从活动队列中选择一个优先级最高的进程来运行。
-
状态切换:当进程被调度运行时,其状态会从TASK_RUNNING(或其他等待状态)切换为实际运行状态,并开始在CPU上执行。
六、时间片耗尽与队列切换
-
时间片耗尽:如果进程的时间片耗尽而还没有完成执行,那么它会被从活动队列中移出,并添加到过期队列中。过期队列中的进程已经耗尽了它们的时间片,等待下次被调度。
-
队列切换:当活动队列中的所有进程都被调度完后,系统会交换活动队列和过期队列的指针,使原来的过期队列成为新的活动队列,并继续调度其中的进程。
二、进程切换
在Linux系统中,由于CPU资源有限,而进程数量众多,因此需要通过调度算法来合理地分配CPU时间给各个进程。runqueue就是这样一个用于管理和调度可运行进程的队列。当进程的时间片用完或遇到其他需要调度的条件时,就会发生进程切换。
一、进程切换的具体流程
1. 触发调度:
• 当进程的时间片用完、进程被阻塞、新进程被创建或唤醒,以及系统负载均衡等情况下,会触发调度器进行进程切换。
2. 选择下一个进程:
• 调度器会根据进程的优先级、调度策略(如CFS完全公平调度算法、RT实时调度算法等)以及当前系统的负载情况,从runqueue中选择一个合适的进程来运行。
3. 保存当前进程的状态:
• 在切换进程之前,系统会保存当前进程的状态,包括CPU寄存器中的值、进程的内存地址空间等。这些信息会被保存在进程的task_struct结构体中的相应字段里。
4. 更新调度信息:
• 系统会更新runqueue中的相关调度信息,如bitmap数组,以反映当前各个优先级队列的状态。
5. 切换进程上下文:
• 进程上下文切换是进程切换的核心步骤。它涉及两个主要部分:进程地址空间切换和处理器状态切换。
• 进程地址空间切换:将下一个进程的虚拟地址空间转换为物理地址空间,并设置到CPU的页表基址寄存器中。这样,当进程访问用户空间地址时,MMU(内存管理单元)会通过这个寄存器来遍历页表,获得物理地址。
• 处理器状态切换:保存当前进程的处理器状态(如寄存器值、程序计数器PC等),并恢复下一个进程的处理器状态。这包括将下一个进程的task_struct地址存放在特定的寄存器中,以便通过current宏找到当前进程。
6. 开始执行下一个进程:
• 完成上下文切换后,CPU开始执行下一个进程。此时,该进程成为当前进程,并继续其之前的执行或开始新的执行。
二、进程切换的注意事项
-
效率:进程切换是一个开销较大的操作,因为它涉及大量的上下文保存和恢复工作。因此,系统需要尽量减少不必要的进程切换,以提高整体性能。
-
公平性:调度器需要确保所有进程都能公平地获得CPU时间,避免某些进程长时间占用CPU而其他进程得不到执行。
-
实时性:对于实时系统来说,进程切换的延迟需要尽可能小,以满足实时任务的时间要求。
进程在Linux内核的runqueue(运行队列)中的退出流程是一个涉及多个步骤和组件的过程。以下是对该流程的具体描述:
三、进程退出
一、进程退出的触发条件
进程退出可以由多种情况触发,包括但不限于:
-
进程正常结束:进程完成了其执行的任务,通过调用exit()函数或从main函数返回等方式正常退出。
-
进程异常终止:进程由于接收到终止信号(如SIGKILL、SIGTERM等)而异常退出。
-
进程被父进程回收:在父进程调用wait()或waitpid()等系统调用时,子进程结束运行并被父进程回收。
二、进程退出时的资源释放
在进程退出之前,系统会释放该进程所占用的系统资源,这包括:
-
关闭文件描述符:释放进程打开的文件资源,确保文件句柄被正确关闭。
-
释放内存:回收进程申请的内存空间,包括堆内存、栈内存以及数据段等。
-
其他资源:如信号量、消息队列、管道等进程间通信资源也会被释放。
三、进程从runqueue中移除
当进程准备退出时,它需要从runqueue中被移除。这通常涉及以下步骤:
-
更新runqueue状态:将进程从runqueue的活动队列(如果它在那里)或过期队列(如果它因为时间片耗尽而被移动到这里)中移除。
-
调整调度器状态:由于进程的退出,调度器可能需要调整其内部状态,如更新优先级队列的bitmap数组等。
四、进程退出后的处理
进程退出后,系统还需要进行一些后续处理,包括:
-
更新进程状态:将进程的状态设置为TASK_DEAD(或类似的表示进程已死的状态)。
-
通知父进程:如果进程是子进程,系统会向其父进程发送SIGCHLD信号,通知父进程子进程已结束。
-
回收进程资源:最终,系统会回收进程的所有资源,并将其从系统中完全移除。这包括释放task_struct结构体等内核数据结构所占用的内存。
五、特殊情况的处理
在多线程环境中,进程的退出还涉及线程的管理。如果进程中的某个线程调用exit()函数退出,整个进程(线程组)都会退出。此时,系统会向进程中的其他线程发送终止信号,并尝试唤醒它们以处理信号并退出。如果线程在不可中断的等待状态(如等待I/O操作完成),则可能需要等待该状态结束后才能退出。