1、Linux的进程简介:
支持多线程的操作系统中,进程是资源分配的最小单位,线程是调度的基本单位。Linux是现代的32位或64位的支持多线程的操作系统,不过Linux是一种以轻量级进程作为线程,多线程任务中的数个线程以线程的组的方式存在,每个线程以轻量级进程实现。
Linux的轻量级进程没有独立的内存空间,进程有独立的内存空间,其中内核级轻量级进程没有内存空间,用户级轻量级进程共享内存空间,进程有自己的mm_struct。
事实上你可以如下理解:linux下,轻量级进程就是指线程,符合POSIX标准规范。至于创建一个线程的细节,你可以参考关于clone这个函数的介绍,除了用clone实现轻量级进程,实际上fork也是调用clone来实现的。
2、Linux进程的描述的数据结构
Linux中的进程以及轻量级进程(以下简称为线程)使用同样的描述符数据结构task_struct(可以在一定程度上理解为PCB)。
task_struct的数据成员主要有:进程状态、内核栈信息、进程使用状态、PID、优先级、锁、时间片、队列、信号量、内存管理信息、文件列表等等与进程管理、调度密切相关的信息。
详细源码参照:内核3.0.6_task_struct源码.txt文件
3、Linux的进程的组织结构、状态以及转换
Linux中,进程的组织结构是双向链表的形式,task中有prev和next的指针,其中链表表头head的位置是0号进程或者说是swapper进程的task_struct。SET_LINKS 和 REMOVE_LINKS 宏分别用于从进程链表中插入和删除一个进程描述符。这些宏考虑了进程间的父子关系。,
进程状态转换图:
◆运行状态(TASK_RUNNING)
当进程正在被CPU执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态(running)。进程可以在内核态运行,也可以在用户态运行。当系统资源已经可用时,进程就被唤醒而进入准备运行状态,该状态称为就绪态。这些状态(图中中间一列)在内核中表示方法相同,都被成为处于TASK_RUNNING状态。
◆可中断睡眠状态(TASK_INTERRUPTIBLE)
当进程处于可中断等待状态时,系统不会调度该进程执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态(运行状态)。
◆不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
与可中断睡眠状态类似。但处于该状态的进程只有被使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态。
◆暂停状态(TASK_STOPPED)
当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。在Linux 0.11中,还未实现对该状态的转换处理。处于该状态的进程将被作为进程终止来处理。
◆僵死状态(TASK_ZOMBIE)
当进程已停止运行,但其父进程还没有询问其状态时,则称该进程处于僵死状态。
当一个进程的运行时间片用完,系统就会使用调度程序强制切换到其它的进程去执行。另外,如果进程在内核态执行时需要等待系统的某个资源,此时该进程就会调用sleep_on()或sleep_on_interruptible()自愿地放弃CPU的使用权,而让调度程序去执行其它进程。进程则进入睡眠状态(TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE)。
只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态。为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断。
4、0号进程的秘密
传说中的swapper进程,其描述符是提前设计好的,进程0创建时,系统设置了时钟,便于进程的调度轮询,process0,以下简称p0。p0具备处理系统调用,这需要通过set_system_gate和system_call(系统调用的总入口)与IDT挂接。
此外,p0还需要与task[64]、GDT、LDT、TSS挂接,并把优先级由0调整为3。Linux规定所有进程(p0除外)必须有其他进程在3特权级别下创建。因而p0需要调用move_to_user_mode()进行优先级翻转。
优先级翻转在响应中断时完成,在响应中断时,系统的各个寄存器的数据按顺序压栈(SS、ESP、EFLAGS、CS、EIP),恢复时按照反序出栈,在相应中断过程中,set_system_gate就是设置int 0x80中断,由3级(iret)反转到0级(iret),同理也可以由0级(iret)翻转到3级(iret)。注:
0级:执行系统代码
3级:执行进程代码
0号进程不是由3级翻转到0级的,没有预先的压栈数据,所以要手工模拟压栈,表示该属性的ss字段的后两位必须是11(3),总值:0x17。
5、1号与2号进程
1号进程由0号进程创建,2号进程由1好进程创建。
2号进程的执行,将意味着shell开始执行,boot过程结束,设备实现怠速。
创建进程过程:
(1)调用fork()函数:
将相应的信息压栈:以便于调用copy_process后初始化p1的TSS。压栈:fork函数偏移值(2)赋给eax,SS、ESP、EFLAGS、CS、EIP入栈,然后DS、ES、FS、EDX、ECX、EBX入栈。
(2)开始执行fork()
在task[64]除[0]以外的数组项初始化清空,调用下面的函数:find_empty_process()获得一个PID和数组中的一个位置。接下来在父进程的内核中继续压栈,5个寄存器的值入栈,为调用copy_process()做准备。
(3)调用copy_process()
创建子进程的task_struct;
复制父进程的task_struct信息到子进程的task_struct之中;
为子进程做struct和tss的个性化设置;
创建子进程的页表,复制父进程页表项到子进程;
共享文件;
设置GDT,将子进程的状态设置为就绪;
注:task_struct和内核栈共同构成task_union:正好占据一页。