进程是正在执行的程序代码的实时结果,是处于执行期的程序以及相关的资源的总称。线程是在进程中活动的对象,内核调度的对象是线程,而不是进程。对Linux系统而言,不区分线程和进程。进程提供两种虚拟机制:虚拟处理器和虚拟内存。线程之间可以共享内存,但每个都拥有各自的虚拟处理器。在Linux中,通过fork创建子进程,子进程通过exit()系统调用终结进程并将其占用的资源释放掉,此时子进程被设置为僵死状态,父进程可以通过wait()或waitpid()系统调用查询子进程是否终结。

  内核把进程的列表存放在叫任务队列(task_list)的双向循环列表中,链表中的每一个项都是类型为task_struct,称为进程描述符。Linux通过slab分配器分配task_struct结构,由于现在用的slab分配器动态生成task_struct,所以只需要在栈低或者栈顶创建一个新的结构体struct thread_info。内核通过进程标识值PID来标识每个进程,内核把每个进程的PID存放在它们各自的进程描述符中,所有的进程都是PID为1的init进程的后代,PID大默认值为32768,这个值越小转一周越快,系统管理员可以通过修改/proc/sys/kennel/pid_max来提高上限。

  进程五种状态标志:TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UN INTERRUPTIBLE、_TASK_TRACED、_TASK_STOPPED。

  进程上下文

  一般程序在用户空间执行,当一个程序执行了一个系统调用或者触发了某个异常,它陷入内核空间,此时,我们称内核“代表进程执行”并处于进程上下文中。除非在此间隙有更高优先级的进程需要执行并且由调度器做出了相应的调整,否则在内核退出的时候,程序恢复在用户空间会继续执行。系统调用和异常处理程序是对内核明确定义的接口,对内核的所有访问都必须通过这些接口。

  进程创建

  子进程和父进程区别仅仅在于PID(每个进程)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(如挂起的信号)。Linux的fork()使用写时拷贝页实现,写时拷贝页是一种可以推迟甚至免除拷贝数据的技术,内核此时并不是赋值整个进程地址空间,而是让父进程和子进程共享同一个拷贝。fork()的实际开销是赋值父进程的页表以及给子进程创建的进程描述符。内核有意会选择子进程先执行,因为一般子进程都会马上调用exec()函数,这样避免了写时拷贝页的额外开销,如果父进程先执行的话,有肯能会开始向地址空间写入。Vfork()除了不拷贝父进程的页表项外,其他跟fork一样,由于vfork语意微妙,系统好不要用这个函数。

  线程创建

  用户空间创建线程可以用clone函数创建,但我们的重点是内核线程,内核常常需要在后台执行一些操作,这种任务可以通过内核线程完成。内核线程和普通进程的区别在于内核线程没有独立的地址空间,它们只在内核空间运行,从来不切换到用户空间去,内核线程和普通进程一样,可以被调度,可以用被抢占。

  内核通过从kthreadd内核进程中衍生出所有新的内核线程的,于是,从现有内核线程中创建一个新的内核线程有两种方法。其一,利用kthread_creat()函数创建,并用wake_up_process()唤醒。其二,直接执行kthread_run()。内核线程启动之后一直运行直到调用do_exit()退出,或者内核的其他不烦你调用kthread_stop()退出。

  在调用exit()结束子进程后,尽管线程已经僵死不能运行了,但系统还保留了它的进程描述符,这样做可以让系统有办法在子进程终结后仍能获得它的信息。所以,进程终结时需要将清理工作和删除进程描述符工作分开执行,我们的exit完成的清理工作,删除进程描述符由父进程中的wait函数完成。Wait函数的标准动作是挂起调用它的进程,直到其中的一个子进程退出。

  孤儿进程

  如果父进程在子进程之前退出,必须有一个机制来保证子进程能找到一个新的父亲,否则这些孤儿进程会在退出时永远处于僵死状态,白白耗费内存。解决这个问题的办法是给子进程在当前线程组内找一个线程作为父亲,如果不行,让init做它们的父进程。