1、进程
进程就是处于执行期的程序,以及相关资源的总称,例如打开的文件、挂起的信号、内核内部数据、处理器状态、具有内存映射的内存地址空间、执行线程、用来存放全局变量的数据段等。
线程是进程中活动的对象。每个线程拥有一个独立的程序计数器、进程栈和一组进程寄存器。
- 内核调度的对象是线程,而不是进程。程序本身并不是进程。
- 传统的Unix系统,一个进程只包含一个线程;现在,多进程程序可以包含多个线程。
- Linux系统对线程和进程并不特殊区分,线程是一种特殊的进程。
进程提供两种虚拟机制:虚拟处理器和虚拟内存。虚拟处理器让进程觉得自己在独享处理器;虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有资源。同一个进程中的线程可以共享虚拟内存,但每个都拥有自己的虚拟处理器。
Linux系统通过调用fork()复制一个现有的进程来创建一个新的进程。调用exec()创建新的地址空间,载入新的程序。最后通过exit()退出执行,释放进程占用的资源。父进程通过wait4()查询子进程是否终结。
进程退出执行后被设置为僵死状态,知道父进程调用wait()或waitpid()。
2、进程描述符及任务结构
任务队列是存放进程列表的双向循环链表。
进程描述符是组成链表节点的task_strcut结构体,包含进程的所有信息,例如打开的文件、进程的地址空间、挂起的信号、进程的状态等。
2.1 分配进程描述符
Linux使用slab分配器分配task_struct结构体。2.6以前的内核将task_struct存放在进程内核栈的尾端,现在动态生成task_struct,在进程的内核栈尾端分配thread_info结构体,成员task指向该进程的task_struct指针。
2.2 进程描述符的存放
内核通过一个唯一的进程标识符(PID)标识每个进程,PID存放在进程描述符中。
current宏查找当前进程的进程描述符,同体系结构宏的实现不同。x86体系获取方式为current_thread_info()->task
,PowerPC直接返回r2寄存器的值。
2.3 进程状态
进程描述符的成员status描述了进程的当前状态。
状态 | 描述 |
---|---|
TASK_RUNNING | 进程是可执行的,它正在执行或者在运行队列中等待执行。是进程在用户空间中执行的唯一可能的状态 |
TASK_INTERRUPTIBLE | 进程正在睡眠,某些条件达成后内核把进程设置为运行。接收到信号提前被唤醒 |
TASK_UNINTERRUPTIBLE | 与可中断状态相同,但接收到信号不会被唤醒 |
__TASK_TRACED | 被其他进程跟踪的进程 |
__TASK_STOPPED | 进程停止执行,没有投入运行也不能投入运行。常发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号的时候 |
2.4 设置进程状态
设置进程状态的几种方法:
set_task_state(task, state); //将任务task的状态设置为state
task->state = state;
set_current_state(state);
2.5 进程上下文
一般程序在用户空间执行,当程序调用了系统调用或者出发异常,就陷入了内核空间。
此时,内核代表进程执行并处于进程上下文。
2.6 进程家族树
所有进程都是PID为1的init进程的后代。
每个task_struct都包含指向其父进程的指针parent,和子进程链表的children。
3、进程创建
fork()通过拷贝当前进程创建一个子进程。
exec()读取可执行文件并将其载入地址空间开始运行。
3.1 写时拷贝
Linux的fork使用写时拷贝页实现。内核并不复制整个进程的地址空间,而是让父子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,各个进程拥有各自的拷贝。
fork的实际开始是复制父进程的页表以及给子进程创建唯一的进程描述符。
3.2 fork()
Linux通过clone()系统调用实现fork(),然后由clone()调用do_fork(),该函数调用copy_process()然后让进程开始运行。
copy_process()函数完成的工作:
- 调用dup_task_struct()为新进程创建与当前进程相同的内核栈、thread_info结构和task_struct。此时父子进程的描述符完全相同。
- 检查并确保创建子进程后 ,进程数目没有超出限制。
- 进程描述符内的许多成员被清0或设为初始值。不是继承而来的进程描述符成员,主要是统计信息。
- 子进程状态被设置为TASK_UNINTERRUPTIBLE。
- 调用copy_flags()更新task_struct的成员flags。PF_SUPERPRIV(是否拥有超级用户权限)被清0,PF_FORKNOEXEC(进程还没有调用exec)被设置。
- 调用alloc_pid()为新进程分配一个有效的PID。
- 根据传递给clone()的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
- 做扫尾工作并返回一个指向子进程的指针。
do_fork()函数在copy_process()函数返回后,唤醒新创建的子进程并让其投入运行。
3.3 vfork()
除了不拷贝父进程的页表项外,vfork()和fork()的功能相同。
子进程作为父进程的一个单独的线程在它的地址空间运行,父进程被阻塞,知道子进程退出或执行exec()。
子进程不能向地址空间写入。
vfork()的实现:
- 调用copy_process时,task_struct的vfork_done被设置为NULL。
- 执行do_fork()时,如果给定特别标志,vfork_done会指向一个特定地址。
- 子进程开始执行后,父进程一直等待,直到子进程通过vfork_done向它发出信号。
- 调用mm_release()用于进程退出内存地址空间,如果vfork_done不为空,向父进程发送信号。
- 回到do_fork(),父进程醒来并返回。
4、线程在Linux中的实现
线程机制提供了在同一程序内共享内存地址空间运行的一组线程,可以共享打开的文件和其他资源。
Linux把所有的线程都当做进程来实现,线程被视为一个与其他进程共享某些资源的进程。
其他系统中,线程被抽象成一种耗费较少资源,运行迅速的执行单元。
4.1 创建线程
线程的创建在调用clone()时需要传递一些参数标志致命需要共享的资源。
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序。
4.2 内核线程
内核线程:独立运行在内核空间的标准进程。内核线程没有独立的地址空间,它们只在内核空间运行。
内核线程只能由其他内核线程创建。创建方法为:
struct task_struct *hthread_create(int (*threadfn)(void *data),
void *data, const char namefmt[], ...)
新创建的进程处于不可运行状态,需要抵用wake_up_process()唤醒它。或者通过调用kthread_run()创建一个进程并让它运行起来。
内核线程调用do_exit()退出,或者内核其他部分调用kthread_stop()退出。
5、进程终结
进程终结时,内核释放它所占有的资源,并告知父进程。
进程的析构发生在调用exit()时,大部分任务靠do_exit()完成。释放和进程相关联的所有资源,进程不可运行并处于EXIT_ZOMBIE退出状态。此时进程存在的唯一目的是向父进程提供信息。
5.1 删除进程描述符
父进程不再关注子进程的信息后,子进程的task_struct才被释放。
wait()函数族挂起调用它的进程,直到其中的一个子进程退出,返回该子进程的PID。
需要释放进程描述符是,release_task()被调用,释放掉进程描述符和所有进程独享的资源。
5.2 孤儿进程
如果父进程在子进程之前退出,成为孤儿的进程会在退出时永远处于僵死状态。解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行就让init做他们的父进程。
在do_exit()中调用exit_notify(),该函数调用forget_original_parent(),后者低啊用find_new_reaper()执行寻父过程。
代码遍历两个链表:子进程链表和ptrace子进程链表。当一个进程被跟踪是,它的临时父亲被设定为调试进程,如果父进程退出了,系统为它和所有兄弟重新找一个父进程。