目录
一、进程及线程简介
二、进程描述符
2.1 进程描述符简介
2.2 分配进程描述符
2.3 进程标识值
2.4 进程状态
2.5 进程上下文
三、进程创建
3.1 写时拷贝
3.2 fork()和vfork()
四、线程
4.1 Linux线程实现
4.2 内核线程
五、进程终结
5.1 删除进程描述符
5.2 孤儿进程
一、进程及线程简介
进程,即处于执行期的程序,是正在执行的程序代码的实时结果。
此时,除了可执行程序代码(代码段,text section),通常还包含其他资源,如:打开的文件、挂起的信号和存放全局变量的数据段等。
线程,即在进程中活动的对象。
每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而非进程。
Linux内核中,对线程的实现与进程并不特别区分,可以将线程理解为特殊的进程。
二、进程描述符
2.1 进程描述符简介
内核将存放进程的列表称——任务队列。 它是双向循环链表,链表的每项(进程)类型为tsak_struct,称——进程描述符,该数据结构定义在<linux/sched.h>,包含着一个进程的所有数据信息。
2.2 分配进程描述符
Linux通过slab分配器动态分配进程描述符结构,以达对象复用和缓存着色的目的。因此只需在进程内核栈底(于向下增长的栈而言)或栈顶(于向上增长的栈而言)创建一个新的结构struct thread_info,该结构定义在<asm/thread_info.h>。
2.3 进程标识值
内核通过唯一进程标识值(PID)以标识每个进程。
PID是一个数值,表示为pid_t隐含类型,实际即int类型。
PID的最大值默认设置为32768(short int最大值),内核把每个进程的PID放在它们各自的进程描述符中。该最大值表示着系统中允许同时存在的进程最大数目,该值越大就代表系统跑一圈越慢,但如果需要可以由系统管理员通过修改/proc/sys/kernel/pid_max以修改上限。
2.4 进程状态
进程描述符里的state域描述了进程的五种状态:
state | 描述 |
TASK_RUNNING(运行) | 进程正在执行,或者在运行队列中等待执行。 |
TASK_INTERRUPTIBLE(可中断) | 进程正在睡眠,或者阻塞以等待某些条件达成。若条件达成则切换为运行。 |
TASK_UNINTERRUPTIBLE(不可中断) | 睡眠,但对信号不做响应。 |
_TASK_TRACED(被其他进程跟踪的进程) | 如通过ptrace对调试程序进行跟踪。 |
_TASK_STOPPED(进程停止执行) | 如接收到SIGSTOP等信号,或者调试期间接收到任何信号,都会进入此状态。 |
若内核经常要调整某个进程的状态,最好使用函数:
set_task_state(task, state);
2.5 进程上下文
可执行程序代码执行时,从一个可执行文件载入到进程的地址空间执行,该地址空间一般为用户空间。当一个进程执行了系统调用或触发异常,则它陷入内核空间,此时,称内核处于进程上下文中,且current宏(指向当前进程,即指向引发对内核访问的进程)有效。
系统调用和异常处理程序是对内核明确定义的接口——对内核的所有访问都必须通过这些接口。
2.6 进程家族树
Linux中所有进程都是PID为1的init进程后代。
内核于系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他相关程序,以完成系统启动的整个过程。
系统的每个进程必有一个父进程,相对应的每个进程也可以有零或多个子进程。拥有同一个父进程的所有进程互称为兄弟进程。进程的关系存放于进程描述符中,为指向tast_struct的parent指针和children的子进程链表。
三、进程创建
3.1 写时拷贝
Linux的fork()通过写时拷贝(copy_on_write)页实现。内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟空间结构。但是系统并不为这些段分配物理内存,它们和父进程共享物理内存。即父子进程的虚拟地址空间是独立的,但是虚拟地址空间映射到同一片物理内存上,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间——资源的复制只有在需要写入时才进行,在此之前以只读方式共享。
3.2 fork()和vfork()
fork()和vfork()都由clone()系统调用实现。
描述\进程创建API | fork() | vfork() |
地址空间 | 子拷贝父 | 子与父共享 |
运行顺序 | 不确定 | 保证子先 |
vfork()保证子进程先执行,因此在它调用 exec()(进程替换) 或 exit()(退出进程)之后父进程才可能被调度运行,如果子进程没有调用 exec()或exit(),程序则会导致死锁。
理想情况下系统最好不调用vfork(),内核可以不实现它。
四、线程
4.1 Linux线程实现
Linux内核中,线程并没有被准备特别的调度算法或是定义特别的数据结构以表征,仅被内核视为一个与其他进程共享某些资源的进程。
每个线程都有自己的task_struct(上文称进程描述符),所以于内核而言,它像是个进程。
线程的创建和普通进程的创建类似,仅在调用clone()时需要传递一些参数标志来指明需要共享的资源(共享地址空间、文件系统资源、文件描述符和信号处理程序):
clone(CLONE VM | CLONE FS | CLONE FILES | CLONE SIGHAND,0);
对比fork()和vfork()的实现:
clone(SIGCHLD,0); //fork()
clone(CLONE | FORKCLONE | VMSIGCHLD,0) //vfork()
clone()的参数标志定义在<linux/sched.h>
4.2 内核线程
独立运行在内核的标准进程——内核线程,与普通进程区别于它们没有独立的地址空间,即它们指向地址空间的mm指针被设置NULL。
没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。
内核线程从不切换到用户空间,其他与普通进程无二,可以被调度和抢占。
在Linux终端运行:
ps -ef
可以看到很多内核线程(带[...]即是),如flush和ksofirqd任务:
五、进程终结
5.1 删除进程描述符
当一个进程终结时,内核必须释放其所占用的资源并告知父进程。一般来说,进程的析构是由其自身调用exit()系统调用引起的。
而在exit()后,尽管进程僵死,但系统任保留其进程描述符。然后父进程获得已终结的子进程信息后,或是通知内核它并不关注子进程信息后,子进程的进程描述符才被释放。
wait()由系统调用wait4()实现,其作用为挂起调用该函数的进程,直到其中一个子进程退出,此时函数返回该子进程PID。
5.2 孤儿进程
若父进程于子进程先退出,则此时子进程称为僵尸进程,须为子进程寻找新的父进程,否则子进程在退出时永远僵死浪费内存,即孤儿进程。可以为子进程在当前线程组(共享同一个用户虚拟地址空间的所有用户线程组成一个线程组)内寻找新的父进程,若不行则让init进程作为新的父进程。