Linux内核学习——进程

进程管理

进程

进程是处于执行器的程序和资源总称。而线程是进程中活动的对象,拥有独立的pc(程序计数器),进程栈和一组寄存器集。内核调度的对象是线程,而不是进程。

进程提供了虚拟机制——虚拟处理器和虚拟内存。这使得进程虽然可能是在共享一个处理器,但看起来像是拥有独立的内存地址和处理器。

在Linux中,进程通过fork创建一个新的进程,而fork又是clone()系统调用的结果。fork()系统调用从内核返回两次,一次回到父进程,一次回到子进程。然后可以调用exec()这组函数载入新的程序,创建新的地址空间,执行程序。

程序可以通过exit()系统调用退出,释放掉该进行所占有的资源。

父进程可以通过wait()查询子进程是否终结,终结的进程处于僵死状态,直到父进程调用wait()相关的系统调用。

进程描述符及任务结构

内核把进程的列表存放在task list的双向循环链表中,链表中每个结点类型为task_struct,该结构比较大,包含了一个进程所有信息。打个比方:

1
2
3
4
5
6
struct task_struct
{
// ...
void *stack; // 指向内核栈的指针
// ...
};

分配进程描述符

使用slab分配器动态生成task_struct,并且在堆栈的低地址区创建一个新的结构struct thread_info,该结构用来存放特定体系结构的汇编代码段需要访问的那部分进程的数据,这是因为Linux内核是支持不同体系的。然后在thread_info中嵌入指向task_struct的指针。

进程描述符的存放

内核通过一个唯一的进程描述符来标识每个进程——PID,最大值默认设置为32768,也就是允许同时存在进程的最大数目。

有些体系结构会把当前的task_struct放在特定寄存器里,有些体系结构则是通过thread_info,然后计算偏移间接查找task_struct,比如x86

进程状态

系统进程必然属于以下五种状态之中:

  • TASK_RUNNING:该状态的进程可以在CPU上运行,或正在执行,或是在运行队列中等待执行,合并了常见操作系统书中——running和ready的两种状态
  • TASK_INTERRUPTIPLE:该进程被阻塞,正在等待某些条件达成,比如等待socket的连接,某些信号的接收
  • TASK_UNINTERRUPTIPLE:在该状态下,即便是接收到某些信号(比如kill ),该进程也不会被中断。这种状态用的比较少,一般用在不想被干扰的进程。比如内核的某些处理流程是不应该被中断,或者在进行硬件操作的时候,一旦进程被中断,就可能造成设备不可控
  • __TASK_TRACED:被其它进程跟踪的进程,例如调用ptrace或者gdb进行调试的时候
  • __TASK_STOPPED:进程停止执行,这种情况发送在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候

设置当前进程状态

使用该函数:

1
2
#include<linux/sched.h>
set_task_state(task, state);

进程家族树

Unix的进程之间存在着继承关系,所有进程都是PID为1的init进程的后代

进程创建

进程创建主要是先在新的地址空间里创建进程,然后读入可执行文件。一般可以将这些步骤分解到fork()和exec()两个函数去做。

写时拷贝

为了提高效率,内核在创建进程的时候采用的是写时拷贝,这种方法共享父进程的数据,在没有发生写入的时候,是不会复制数据,拥有新的拷贝。

线程在Linux中的实现

其实在Linux中是没有线程这一概念的,Linux把所有线程当做进程来实现,所以每个线程都会有自己的task_struct,只是线程之间会共享某些资源。

线程创建

线程的创建和进程类似,只是在线程创建的时候提供某些标识参数,以确保它们共享某些资源。

内核线程

内核线程可以用来执行某些内核在后台的工作,内核线程是没有独立的地址空间的,它只在内核空间运行,不会被切换到用户空间。

进程终结

进程的终结可能是显式调用exit系统调用,也可能是从程序主函数返回。然后进程会调用do_exit()去释放相关资源,进程进入EXIT_ZOMBIE状态,该状态的目的就是向父进程提供信息,由父进程检索或者通知内核后,释放进程的剩余内存,归还给系统。

如果子进程处于zombie状态,但是父进程已经退出了,会给子进程在当然的线程组中找一个线程作为父亲,或者使用init进程作为父亲。一旦找到了新的父进程,就不会有zombie进程占用内存的问题了,init进程会例行调用wait()来检查子进程,清除zombie进程。