进程

内核调度的对象是线程,而不是进程。Linux系统的线程实现非常特别:它对线程和进程并不特别区分。
进程提供两种虚拟机制,虚拟处理器与虚拟内存。一个是进程独享处理器的假象一个是独享内存资源的假象。线程之间共享虚拟内存,各自有虚拟处理器。
父进程调用fork()复制现有进程创建一个全新的进程子进程。

每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。Linux系统的线程实现非常特别:它对线程和进程并不特别区分。
进程提供两种虚拟机制,虚拟处理器与虚拟内存。一个是进程独享处理器的假象一个是独享内存资源的假象。线程之间共享虚拟内存,各自有虚拟处理器。
进程的另一个名字是task,Linux内核通常把进程也叫做任务。

进程描述符及任务结构

内核把进程的列表存放在任务列表(task list)的双向循环列表中。链表的每一项都是类型为task_struct称为进程描述符的结构中,该结构相对较大,包含的数据能够完整的描述一个正在运行的程序:打开的文件,进程地址空间,挂起信号,进程状态及其他。

分配进程描述符

Linux通过slab分配器分配task_struct结构,这样能够对象复用和缓存着色。
使用slab动态生成task_struct需要用到结构thread_info,其task域有指向实际进程描述符的指针。每个任务的thread_info在它的内核栈的尾端分配。

进程描述符的存放

内核通过唯一标识符PID(process indentification value)来标识每个进程,PID存放在进程描述符。为了兼容性设置其为short int型,故其最大值为32768.但是对于大型服务器需要更大值。可以不考虑兼容性修改/proc/sys/kernel/pid_max提高上限。

内核访问任务需要获取指向task_struct的指针,故通过宏找到当前正在运行的进程描述符的速度就尤为重要。硬件的结构体系不同,宏的实现不同。例如有的体系结构有专用寄存器用于加快访问task_struct的速度,x86结构寄存器并不富余,就只能通过计算偏移间接查找task_struct。

进程状态

task_struct的state域描述当前状态,必为五中之一

  • TASK_RUNNING(运行)在运行或在运行队列等待
  • TASK_INTERRUPIBLE(可中断)被阻塞
  • TASK_UNINTERRUPTIBLE(不可中断)接到信号也不会唤醒
  • _TASK_TRACED被其他进程跟踪
  • _TASK_STOPPED停止执行

设置当前的进程状态

调整某个进程状态

set_task_state(task,state);

进程上下文

可执行代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行,一般在用户空间执行,执行系统调用或触发异常陷入内核空间。对内核的访问必须经过明确定义的接口。

进程家族树

Linux和Unix一样所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程读取系统初始化脚本(initscript)并执行其他相关程序最终完成系统启动整个过程。init进程的进程描述符是作为init_task静态分配的。
对于当前task_struct,可以通过task_struct类型parent指针访问其父进程(必有一个),还包含一个children的子进程链表。

进程创建

许多其他操作系统:首先在新的地址空间创建进程,读入可执行文件,最后开始执行。
Unix:首先通过fork()拷贝当前进程创建一个子进程,父子进程区别仅限于PID,PPID(子进程将其设为父进程PID值)和某些资源统计量。exec()负责读取可执行文件并将其载入地址空间运行。如果 exec 调用成功,调用进程将被覆盖,然后从新程序的入口开始执行。这样就产生了一个新的进程,但是它的进程标识符与调用进程相同。这就是说,exec 没有建立一个与调用进程并发的新进程,而是用新进程取代了原来的进程。

写时拷贝

Linux的fork()采用写时拷贝,只有子进程在父进程进行写入时再进行拷贝,其他的时候是只读共享。所以fork()的实际开销就是复制父进程页表以及创建PID。

fork()

Linux通过clone()系统调用实现fork()
fork()-调用->clone()-调用->do_fork()(完成创建的大部分工作)-调用->copy_process()
copy_process工作:

  1. 调用dup_task_strucr()为新进程创建一个内核栈、thread_info和task_struct,此时,父子进程描述符完全一样。
  2. 检查并确保新创建这个进程后进程数目没有超出资源限制。
  3. 子进程着手于区别父进程。task_struct的许多成员被清零或设为初始值。
  4. 子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。
  5. copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERRIV标志被清0,表示进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
  6. 调用alloc_pid()为新进程分配一个分配一个有效的PID。
  7. 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信息处理函数、进程地址空间和命名空间等。一般,这些资源会所有线程共享,否则资源对每个进程是不同的,因此拷贝到这里。
  8. 最后,copy_process做扫尾工作并返回一个指向子进程的指针。

如果copy_process()成功返回do_fork(),新创进程被唤醒且内核有意让其首先执行。因为一般子进程都会马上调用exec()函数,可以避免父进程首先执行可能的写入造成的额外开销。

vfork()

除了不拷贝父进程的页表项以外,vfork()和fork()功能相同。基本没啥用。

线程在Linux中的实现

Linux中,线程具有task_struct,它看起来就是普通进程,只是和其他一些进程共享某些资源,如地址空间。对Linux来说,线程只是进程间共享资源的手段。

创建线程

线程创建于进程类似,只是在调用clone时需要传递一些参数标明需要共享的资源。创建Linux进程所花时间和其他操作系统创建线程要少。

内核线程

独立运行在内核空间的标准进程,和普通进程区别在于没有独立地址空间。只在内核空间运行,和普通进程一样,可以被调度,也可以被强占。
新的任务是由kthread内核进程通过clone()系统调用而创建的。
内核线程启动后就一直运行到调用do_exit()退出或其他部分调用kthread_stop()退出。

进程终结

终结时,释放资源并告诉父进程。
可以调用exit()主动终结,也可以异常被动终结,但大部分靠do_exit()来完成
do_exit():

  1. 将task_struct标志设为PF_EXITING
  2. 调用del_time_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队也没有定时器处理程序在运行。
  3. 如果BSD的记账功能是开启的,do_exit()调acct_update_integrals()来输入记账信息。
  4. 调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个进程空间没有被共享),就彻底释放它们。
  5. 调用sem_exit()函数,如果进程排队等候IPC信号,它则离开队列。
  6. 调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为0,那么久代表没有进程在使用相应的资源可以释放。
  7. 接着把存放在task_struct和exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作,退出代码存放在这里供父进程随时检索。
  8. 调用exit_notufy()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或为init进程,并把进程状态(存放在task_struct中的exit_state中)设成EXIT_ZOMBIE。
  9. do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程执行的最后一段代码。do_exit()永不返回。

至此进程仅剩的内存就是内核栈,Thread_info 和task_struct结构。此时进程存在的唯一目的数向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关信息后,内存释放。

删除进程描述符

进程终结时的清理工作和进程描述符的删除被分开进行,这样做可以让系统有办法在子进程终结后仍能获得它的信息。过程如上段。

孤儿进程造成的进退维谷

如果父进程在子进程之前退出,必须有机制来保证子进程能够找到一个新的父亲。解决方法是给子进程在当前线程组内找一个线程做父亲,如果不行,就让init做父进程。