文章目录
1 进程管理
【进程】:处于执行期的程序。创建进程需要打开文件、挂起信号、内核内部数据、处理器状态、映射内存地址空间等;
- 进程间采用
虚拟处理器
以及虚拟内存
(可以在线程中共享); - fork从内核中返回
两次
,一次回到父进程,一次回到子进程;该函数实际是clone
的系统调用; - 避免子进程没有被父进程回收而成为僵尸进程,父进程需要调用
wait或waitpid
;内核实现为wait4()
;
【线程】:在进程中存活,有独立的程序计数器、进程栈、寄存器、栈、栈指针
;是内核调度的对象;
1.1 进程描述符及任务结构
【任务结构task_struct(进程描述符)】:该结构为双向循环链表
,内核将进程存放于此;(于<linux/sched.h>
)
- 该结构体在32位上大概又
1.7KB
,进程描述符可完整描述正在执行的程序,不同版本的有差异;
The Slab Allocator in the Linux kernel
【分配进程描述符】:内核为了让对象复用和缓存着色(提高进程的创建速度)通过slab分配;
- 复用,避免频繁动态分配和释放带来的资源消耗;
【进程描述符的存储位置】
-
【做法一】:将task_struct存放在内核栈的尾部,通过栈指针计算出它的位置,以此避免使用单独的寄存器记录;
-
【做法二】:2.6后采用slab分配器动态生成,故只需在栈顶或底创建一个struct thread_info即可,该做法让汇编中计算偏移更容易且适用于x86寄存器不富裕的机器;
- 每个thread_info在它的内核栈的尾端分配,其中的task指向task_struct;
-
【做法三】:若寄存器较为富裕的机器,可单独使用寄存器来记录;
-
x86 架构具有 8 个通用寄存器 (GPR)、6 个段寄存器、1 个标志寄存器和一个指令指针。64 位 x86 有额外的寄存器。
struct thread_info {
struct pcb_struct pcb; /* palcode state */
struct task_struct *task; /* main task structure */
unsigned int flags; /* low level flags */
unsigned int ieee_state; /* see fpu.h */
struct exec_domain *exec_domain; /* execution domain */
mm_segment_t addr_limit; /* thread address space */
unsigned cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
int bpt_nsaved;
unsigned long bpt_addr[2]; /* breakpoint handling */
unsigned int bpt_insn[2];
struct restart_block restart_block;
};
【进程描述符的标识】
- 内核通过唯一的标识值或PID(pid_t)来标识每个进程;
- 其中PID_MAX=32768,可通过
proc/sys/kernel/pid_max
修改;
- 其中PID_MAX=32768,可通过
【获取进程描述符的current】
- 内核中通过current来获取task_struct;
- 在x86上,current将栈指针的后13个有效位屏蔽,用于计算thread_info的偏移(于current_thread_info中操作);
- 当current获取到thread_info在通过task提取task_struct;
【进程状态】:task_struct->state
TASK_RUNNING
:该进程为可执行,正在运行或在运行队列等待;TASK_INTEREUPTIBLE
:被阻塞,需要某条件才可触发为运行,可被中断;TASK_UNINTERRUPTIBLE
:不可中断,等待中且不受信号干扰;__TASK_TRACED
:被其他进程跟踪的进程;__TASK_STOPPED
【D】:进程停止执行,不能运行;在调试中,只要收到信息就进入该状态;
【设置当前进程状态】:通过set_task_state(task, state)
,该函数在SMP下会设置内存屏障,避免程序执行结果出现问题;
- 否则可使用
task->state
来设置;
【进程上下文】:当程序执行系统调用
或触发异常
将会进入到内核态,需要记录程序的状态就是上下文;
【进程家族树】:init(pid=1)进程在内核最后阶段启动,读取系统初始化脚本并执行其他相关程序,后续所有进程的父进程;
- init进程由
init_task
静态分配; - 可通过
list_entry(task->tasks.next/prev, struct task_struct, tasks)
,遍历获取进程获取到所有的进程(本身是一个双向链表
);
1.2 进程创建
- UNIX使用
fork
和exec
两个函数来完成; - 【fork】:拷贝当前进程创建子进程;
- 子父区别:pid、ppid、资源和统计量;
- 【exec】:读取可执行文件并载入地址空间开始运行;
【写时拷贝】:让父子进程共享同一个拷贝
,写时数据复制
;之前以只读共享,若fork后立即调用exec便无需复制,节省不必要的开销;
- 即fork开销为复制父进程页表、创建唯一的进程描述符;该优化避免拷贝大量不被使用的数据;
【创建进程函数】:fork、vfork、__clone调用clone在调用do_fork
;
1.2.1 fork
clone(SIGCHLD, 0)
调用do_fork;- do_fork(kernel/fork.c):该函数创建大部分工作,调用
copy_process
函数;
【copy_process执行】:
- 调用
dup_task_struct()
为新进程创建内核栈、thread_info、task_struct与当前进程值相同,此时父子进程描述符相同; - 检查,确保进程数不超出资源限制;
- 子进程描述符成员部分设为初始值或被清0;
- 子进程设置为
TSAK_UNINTERRUPTIBLE
状态,保证不投入运行; copy_process
调用copy_flags
更新task_struct
的flags成员,表示进程是否由root权限PF_SUPERPRIV
清0,设置是否调用exec的PF_FORKNOEXEC
;- 调用
alloc_pid
分配新的PID; - 根据clone的参数,copy_process拷贝或共享打开文件、文件系统信息、信号处理函数、进程地址空间、命名空间等;
copy_process
返回指向子进程的指针;- 若成功,返回do_fork,子进程被唤醒运行,
内核有意选择子进程首先执行
,为了若子进程马上调用exec可避免写时拷贝的额外开销;若父进程先执行,可能会开始向地址空间写入;
1.2.2 vfork
【vfork】:不拷贝父进程的页表项,其余与fork相同;出现写时拷贝后,不需要该函数;
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)
- 实现向clone函数传入
特殊标识
来实现; - 调用
copy_process
时,task_struct的vfork_done置为NULL; - 执行do_fork,若给特定标志,则
vfork_done
会指向一个特定地址; - 子进程先开始执行后,父进程一直等待,直到子进程通过vfork_done指向它发送的信号;
- 调用
mm_release()
时,退出内存地址空间,并检查vfork_done是否为空,不为空则向父发送信号; - 回到do_fork,父进程唤醒并返回;
- 子进程在新的地址空间运行,父进程也恢复在源地址运行;
1.2.3 线程
【实现】:同一程序中的线程共享内存地址,内核没有线程概念,被视为一个与其他进程共享资源的进程,都拥有独自的task_struct,在同一进程下的线程,创建时会指定共享某些资源;
【创建】:类似创建进程,在clone传入特殊参数;
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
【clone参数标识(linux/sched.h)】
CLONE_FILES
:父子进程共享打开文件;CLONE_FS
:父子进程共享文件相同信息;CLONE_IDLETASK
:将PID设置为0,供idle进程使用;CLONE_NEWNS
:为子进程创建新的命名空间;CLONE_PARENT
:指定子进程与父进程有用同一个父进程;CLONE_PTRACE
:继续调试子进程;CLONE_SETTID
:将TID回写到用户空间;CLONE_SETTLS
:为子进程创建新的TLS;CLONE_SIGHAND
:父子进程共享信号处理函数及被阻断的信号;CLONE_SYSVSEM
:父子进程共享System V SEM_UNDO语义;CLONE_THREAD
:父子进程放入相同的线程组;CLONE_vfork
:调用vfork,所以父进程准备睡眠等待子进程将其唤醒;CLONE_UNTRACED
:防止跟踪进程在子进程上强制执行CLONE_PTRACE;CLONE_STOP
:以TASK_STOPPED状态开始进程;CLONE_SETTLS
:为子进程创建新的TLS;CLONE_CHILD_CLEARTID
:清除子进程的TID;CLONE_PARENT_SETTID
:设置父进程的TID;CLONE_VM
:父子进程共享地址空间;
【内核线程】:没有独立的地址空间,只在内核空间上运行,可被调度,可被抢占;
- 【内核线程和进程的区别】:内核线程没有独立的地址空间(mm值向NULL),只能在内核运行,但可被调度、抢占;
- 内核通过
kthread
创建新的内核线程; struct task_struct *kthread_create(int (*threadfn)(void* data), void* data, const char namefmt[], ...)
- kthread调用clone,进程命名为namefmt,新创建不会主动运行,需要通过wake_up_process()唤醒;
- 若创建即允许,可通过
kthread_run(int (*threadfn)(void* data), void* data, const char namefmt[], ...)
实现; - 运行至
do_exit()
退出或kthread_stop()
退出;
1.4 进程终结
- 需要将资源释放,并将通知父进程,一般靠
do_exit()
实现<kernel/exit.c>; - 进程的
清理工作
和进程描述符的删除
被分开执行;
【do_exit完成的工作】:
- 将task_struct设置为
PF_EXITING
; - 调用
del_timer_sync
删除任一内核定时器,确保没有任务在执行; - 若BSD进程记账功能开启,则调用acct_update_integrals输出记账信息;
- 调用
exit_mm
释放进程占用mm_struct,若没有别的进程使用(共享),就彻底释放; - 调用
exit_files()
和exit_fs()
来递减文件描述符、文件系统数据的引用计数;若为0,则释放; - 将存放task_struct的exit_code成员中任务代码置为由exit提供的退出代码,供父进程随时检索;
- 调用exit_notify向父进程发送信息,给子进程找养父,并将进程状态exit_state设置为
EXIT_ZOMBIE
; - do_exit调用schedule切换到新的进程,因为处于EXIT_ZOMBIE状态的进程不会在被调度,终止;
- 当父进程获得终结的子进程信息,父进程通知内核将持有的task_struct释放;
【删除进程描述符】:使用wait函数通过wait4实现,将挂起调用它的进程,直到其中一个子进程退出
- 调用
__exit_signal
进而调用_unhash_process()
,在调用detach_pid()从pidhash删除该进程,且从任务队列删除该进程; - __exit_signal释放目前僵尸进程使用的剩余资源,并统计记录;
- 若该进程为进程组内最后一个,领头已死,则release_task要通过僵死的领头父进程;
- release_task调用put_task_struct释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存;
- 为止,进程描述符和所有进程独占资源都被free;
【孤儿进程】:父进程在子进程退出前,保证子能找到一个新的父进程,否则成为僵尸进程,浪费内存;
-
【方法】:在当前线程组找一个线程为父亲,或让init;
-
当do_exit()后会调用exit_notify,该函数调用forget_original_parent后调用find_new_reaper来寻父;
-
若find_new_reaper找到养父进程,只需要遍历子进程为它们设置新的父进程;
-
调用ptrace_exit_finish为ptraced的子进程寻父;
代码中将有子进程链表
和ptrace子进程链表
,给每个子进程设置新的父进程;
当一个进程被跟踪时,它的临时父亲设定为调试进程,若父进程退出,则系统为它和它的所有兄弟进程重新找一个父进程;在以前版本,需要遍历所有系统就能成来找这些子进程;
【改进】在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程,用两个相对较小的链表减轻遍历带来的消耗;
2 进程调度
2.1 多任务
能够同时并发地交互执行多个进程的系统;
【抢占】:调度程序决定何时暂停一个进程的运行,让其他程序能够运行;
【非抢占】:会一直执行;
【进程时间片】:程序抢占之前设置好的,分配给每个可运行进程的处理器时间段,避免个别进程独占系统资源;
2.2 Linux进程调度
- 于2.5后,O(1)调度器很完美,但对于交互进程有些不足;
- 2.6,提出反转楼梯最后期限调度算法RSDL,使用队列理论——完全公平调度;
2.3 策略
2.3.1 I/O消耗型和处理器消耗型
【I/O消耗型】:进程大多时间来提交或等待I/O请求,经常处于可运行状态,但只运行一小会,更多的时间在等待而被阻塞;
【处理器消耗型】:大多时间用在执行代码上,调度器一般尽量降低它们的调度频率,而延长其运行时间;
Linux为了保证交互式的性能,更倾向于优先调度I/O消耗型进程;
【进程优先级】:一般选择优先级最高且时间片未用尽的进程;
- 【nice值】:-20到+19,默认0,越大优先级越低;
- 【实时优先级】:可配置,0~99,值越大优先级越高;
【时间片】:表明进程被抢占前所能持续运行的时间;
- I/O消耗型不需要长的时间片,而处理器消耗型需要;
- CFS没有直接分配时间片到进程,而是将处理器的使用比划分给了进程,是将处理器使用比例分给进程;由此进程获得时间是与系统负载相关,但也受nice值的影响;
【调度策略活动】:
当一个文本编辑器和视频编码程序,CFS发送文比视运行的时间片短的多,但要求满足公平调度,在文投入运行时,会立刻抢占视;但文没有消耗掉承诺的处理器使用比,故当文需要投入运行到时候,会立即将其投入运行;
2.4 Linux调度算法
【调度器类】:以模块方式提供便于在不同类型的进程能够选择不同算法;
- 允许多种不同的可动态添加的调度算法并存,按优先级遍历;
【Unix进程调度】:
-
【将nice值映射到时间片,需要将nice单位值对应到处理器的绝对时间】:会导致进程切换无法最优化;
-
【相对nice值】:所带来的效果取决于nice的初始值;
- 如O(1)调度算法,nice为0,另一个为1则时间片为100ms和95ms;若为18和19nice,则时间片为10ms和5ms;
-
【nice时间片映射,需要绝对时间片】:
【公平调度】:每个进程都能获取1/n(可运行进程的数量)的处理器时间;
- 【做法】:允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程,不分配时间片;
- 在所有可运行进程总数基础上计算进程应该运行多久,而nice作为权重;
- 引入最小粒度1ms,避免任务量无限时,获得的是处理器使用比为0;
- 任何进程获取的处理器时间由它自己和其他所有可运行进程nice值的相对差值决定;
【调度实现】:<kernel/sched_fair.c>由时间记账、进程选择、调度器入口、睡眠和唤醒;
-
【时间记账】:所有的调度器都对进程运行时间做记账;
- 【调度器实体结构】:CFS没有时间片概念,但也需要维护时间记账,由于要确保每个进程只在公平分配给它的处理器时间内运行
struct_sched_entity
来追踪进程运行记账; - 【虚拟实时ns】:测量给定进程的运行时间,可逼近CFS模型的理想多任务处理器;CFS使用其来记录一个程序运行多久;
update_curr()
可知道下一个运行的进程;
- 【调度器实体结构】:CFS没有时间片概念,但也需要维护时间记账,由于要确保每个进程只在公平分配给它的处理器时间内运行
-
【进程选择】:选择具有最小vruntime的任务;
-
CFS使用红黑树来组织科运行进程队列,可迅速找到最小vruntime值的进程;
-
【挑选下一个任务】:运行rbtree树中最左边叶子节点所代表的那个进程
__pick_next_entity()
;该函数不会遍历树,该值已缓存在rb_leftmost中;若没有可运行进程,CFS调度器便选择idle; -
【向树中加入进程】:发生于在进程变为可运行状态或fork调用第一次创建进程时;
enqueue_entity()
该函数更新运行时间和统计数据,在调用__enqueue_entity()进行繁重的插入操作,把数据项真正插入到红黑树中,并缓存最左叶子节点; -
【从数中删除进程】:发送在进程堵塞为不可运行或结束运行;需要更新最左节点;
-
【调度器入口】:schedule(),选择哪个进程阔以运行,何时将其投入运行;需要和一个调度类管理来选择进程;
pick_next_task
;
-
-
【睡眠和唤醒】:休眠必须以轮询的方式;从红黑树中移除,放入等待队列,在调用schedule和执行一个其他进程;唤醒被设置为可执行状态,从等待队列移动到可执行红黑树中;
- 【等待队列】:由等待某些事件发送的进程组成的简单链表;将自己加入到等待队列:
- 调用宏DEFINE_WAIT()创建一个等待队列的项;
- 调用add_wait_queue把自己加入到队列中,条件满足即唤醒;
- 调用prepare_to_wait将进程状态变更;
- 若为TASK_INTERRUPTIBLE,则信号唤醒进程,伪唤醒;
- 当进程被唤醒时,检查条件是否为真,若是,则退出循环,不是则再次加入schedule;
- 条件满足吼,进程将自己设置为TASK_RUNNING并调用finish_wait移除等待队列;
- 【唤醒】:wake_up()调用try_wake_up()唤醒,并调用enqueue_task()放入红黑树;
- 【等待队列】:由等待某些事件发送的进程组成的简单链表;将自己加入到等待队列:
【抢占上下文】:从一个可执行进程切换到另一个可执行进程,由context_switch()处理;当新进程被准备投入运行时:
- 调用switch_mm()将虚拟内存从上一个进程映射切换到新进程;
- 调用switch_to()从上一个进程的处理器状态切换到新进程的处理器状态(保存、恢复栈信息、寄存器信息即体系结构相关的状态信息);
由于用户代码显示调用,可能会死循环,故内核提供了一个need_resched标志来表明是否需要重新执行一次调度;
- 【用户抢占】:当内核返回用户空间时,若need_resched被设置,会导致schedule被调用,会发生用户抢占;一般在从系统调用返回用户空间时或从中断处理程序返回用户空间时;
- 【内核抢占】:2.6后引入,若没有锁,则抢占;为每个进程的thread_info引入计算器,用锁加1;内核抢占一般发生在:
- 中断处理程序正在执行,且返回内核空间之前;
- 内核代码再一次具有可抢占性的时候;
- 若内核中的任务显示地调用scheduler;
- 若内核中的任务阻塞;
【实时调度策略】:
- 【SCHED_FIFO】:简单、先入先出,不适用时间片;会比任何SCHED_NORMAL级的进程都先得到调度;该级进程处于可执行,则会一直执行;知道它自己受阻塞或显示地释放处理器为止,只有更高级别的任务才能抢占;
- 【SCHED_RR】:在耗尽事先分配给它的时间后不再继续执行,是带有时间片的SCHED_FIFO