1.进程
进程提供两种虚拟机制:虚拟处理器和虚拟内存。
fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。通常,创建新的进程都是为了立即执行新的,不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。在现代linux内核中,fork()实际上由clone()系统调用实现。
最终,程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否终结。
2.进程描述符及任务结构
内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中每一项都是类型为task_struct,称为进程描述符的结构。
2.1分配进程描述符
linux通过slab分配器分配task_struct结构。2.6以前的内核中,各个进程的task_struct存放在它们内核栈的尾端。这样做方便计算出它的位置,节约寄存器。由于现在用slab分配器动态生成task_struct,而在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建新的结构struct thread_info。
每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。
2.2进程描述符的存放
进程标识符默认最大32768,可以通过修改/proc/sys/kernel/pid_max来进行修改。
current宏用于查找当前正在运行进程的进程描述符。在x86系统上,当栈大小为4KB时,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移。
2.3 进程状态
进程的五种状态:
- TASK_RUNNING(运行):进程是可执行的,或者正在执行,或者在运行队列中等待执行。
- TASK_INTERRUPTIBLE(可中断):进程正在睡眠(也就是被阻塞),等待某些条件的达成。
- TASK_UNINTERRUPIBLE(不可中断):除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。通常在进程必须在等待时不受干扰或等待时间很快就会发生时出现。
- __TASK_TRACED:被其他进程跟踪的进程,例如ptrace调试。
- __TASK_STOPPED:进程停止执行;进程没有投入运行也不能投入运行。通常发生在接收到SIGSTOP,SIGSTP,SIGTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
2.4 设置当前进程状态
内核经常需要调整某个进程的状态。这时最好使用settaskstate(task, stat)函数。
setcurrentstate(state)等同于settaskstate(current, state);
2.5 进程上下文
当一个程序执行了系统调用或者触发了某个异常,它就陷入内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。
2.6 进程家族树
访问某个进程的子进程:
struct task_strcut *task;
struct list_head *list;
list\_for\_each(list, &
current->children) {
task = list_entry(list, struct task_struct, sibling);
/*task现在指向当前的某个子进程*/
}
init进程的进程描述符是作为init_task静态分配的。
遍历祖先节点进程:
struct task_struct *task;
for(task = current; task != &init_task; task = tsk->parent);
/*task 现在指向init*/
由于任务队列本来就是个双向循环列表,所以可以很容易遍历所有进程。
获取链表的下一个进程和前一个进程:
list_entry(task->tasks.next, struct task_struct, tasks);
list_entry(task->tasks.prev, struct task_struct, tasks);
这两个可以通过next_task(task)宏和prev_task(task)宏实现。
3.进程创建
进程的创建包括两个部分:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。exec()负责读取可执行文件并将其载入地址空间开始执行。
3.1 写时拷贝(copy-on-write)
传统fork()系统调用直接把所有的资源复制给新创建的进程。linux使用写时拷贝页实现。也就是在创建子进程之后,并不拷贝父进程的数据,而是父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
3.2 fork()
linux通过clone()系统调用实现fork()。fork(),vfork(),__clone()库函数都跟据各自需要的参数标志调用clone(),然后由clone()去调用do_fork()。
do_fork完成了创建中的大部分工作,该函数调用调用copy_process()函数,然后让进程开始运行。
copy_process()运行如下:
- 调用dup_task_struct为新进程创建一个内核栈,thread_info结构和task_struct,这些值与当前进程的值相同。此时,父子进程的描述符是完全相同的。
- 检查并确保创建这个子进程后,当前用户所拥有的进程数目没有超过给它分配的资源的限制。
- 子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值,主要是统计信息,而不是进程而来的进程描述符成员。task_struct中的大多数数据都依然未被修改。
- 子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。
- copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
- 调用alloc_pid()为新进程分配一个有效的PID。
- 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。
- 最后,copy_process()做扫尾工作并返回一个指向子进程的指针。
最后返回do_fork()。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样就可以避免写时拷贝的额外开销,如果父进程首先执行,有可能会开始向地址空间写入。
3.3 vfork()
除了不拷贝父进程的页表项以外,vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。现在vfork已不太支持。
3.4 内核线程
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,它们只在内核空间运行,从来不切换到用户空间。内核进程和普通进程一样,可以被调度,也可以被抢占。内核线程也只能由其他内核线程创建。
4 线程在linux中的实现
Linux中的线程比较独特。从内核角度来说,它并没有线程这个概念。Linux把所有的线程都当作进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。
4.1 创建线程
线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
普通fork:
clone(SIGCHLD, 0);
vfork:
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
4.2内核线程
内核经常需要在后台执行一些操作。这种任务可以通过内核线程完成--独立运行在内核空间的标准进程。内核线程只能由其他内核线程创建。内核线程首先创建,然后启动,然后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出。
5进程终结
当一个进程终结时,内核必须释放它所占有的资源并告知父进程。
一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显示调用,也可能隐式从某个程序的主函数返回。
进程无论怎么终结,都是调用do_exit()来完成。调用之后,与进程相关联的所有资源都被释放了(假设该进程是这些资源的唯一使用者)进程不可运行并处于EXIT_ZOMBIE退出状态。它占用的所有内存就是内核栈,thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内核被释放,归还系统。
5.1 删除进程描述符
在调用do_exit()后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。前面说过,这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
wait()这一族函数都是通过唯一的系统调用wait4()来实现的。它挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。
5.2 孤儿进程造成的进退维谷
如果父进程先于子进程退出,那么给子进程在当前线程组内找一个线程作为父亲,如果不行,则让init做它们的父进程。