目录
一、进程及线程简介
进程,即处于执行期的程序,是正在执行的程序代码的实时结果。
此时,除了可执行程序代码(代码段,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进程作为新的父进程。