Linux内核设计与实现 进程管理

本文详细介绍了进程与线程的概念,包括进程描述符task_struct、进程状态、进程创建与终结的过程,以及线程在Linux中的实现方式。同时,还探讨了内核线程的作用和进程家族树的结构。

进程

进程是处于执行期的程序(目标码存放在某种存储介质上),但进程不仅仅局限于一段可执行程序代码,通常还要包含其他资源,像打开的文件(内核为每个进程分配一个打开文件表),挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程。

总之,进程就是正在执行的程序代码的实时结果

执行线程

简称线程,在现代操作系统中,进程是资源分配和处理机调度的基本单位,而线程是处理机调度的基本单位。线程是在进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。

进程描述符及任务结构

进程描述符task_struct

内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中,链表的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构。进程描述符中包含一个具体进程的所有信息。

进程的另一个名字是任务(task)。Linux内核通常把进程也叫做任务,但任务通常指的是从内核观点所看到的进程

#define task_thread_info(task)  ((struct thread_info *)(task)->stack)
//task_struct中包含的大致属性
struct task_struct{
    unsigned long state;  //进程状态,-1 unrunnable, 0 runnable, >0 stopped //
    int prio;   //任务调度优先级
    unsigned long policy;  
    struct task_struct *parent;  //指向父进程的指针,因为要共享正文段,复制数据段和栈区
    struct list_head tasks;
    pid_t pid;   //进程号
}

分配进程描述符

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色。
我的理解是,task_struct作为一个常用且通用的数据结构,到用的时候再申请内存分配给进程的话,可能效率不太高,而且可能会遇到内存空间不足的情况。因此引入slab分配器,将类似task_struct的通用数据结构缓存起来,这样可以保证内核在需要为进程分配task_struct时的高效性。有点类似线程池的概念,通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗,Unix的一个特点就是进程创建迅速。

slab分配器:通用数据结构缓存层,slab分配器试图在几个基本原则之间寻求一种平衡:

  • 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们
  • 频繁分配和回收必然会导致内存碎片,为了避免这种现象,空闲链表的缓存会连续的存放。因为已释放的数据结构又会放回空闲链表,因此不会导致碎片。
  • 回收的对象可以立即投入下一次分配,因此对于频繁的分配和释放,空闲链表能够提高其性能

用slab分配器动态生成task_struct,只需要在栈底(向下增长的栈)或栈顶(向上增长的栈)创建一个新的结构struct thread_info。

struct thread_info {
    struct task_struct    *task;           /* main task structure */
    struct exec_domain    *exec_domain;    /* execution domain */
    __u32            flags;                /* low level flags */
    __u32            status;               /* thread synchronous flags */
    __u32            cpu;                  /* current CPU */
    int            preempt_count;          /* 0 => preemptable, <0 => BUG */
    mm_segment_t            addr_limit;
    struct restart_block     restart_block;
    void __user             *sysenter_return;
#ifdef CONFIG_X86_32
    unsigned long previous_esp; /* ESP of the previous stack in
                                   case of nested (IRQ) stacks
                                   */
    __u8                supervisor_stack[0];
#endif
    unsigned int        sig_on_uaccess_error:1;
    unsigned int        uaccess_err:1;    /* uaccess failed */
};

可以清楚地看到,thread_info中有一个task指针,这是指向任务实际的进程描述符的指针。

进程描述符的存放

内核通过一个唯一的进程标识值–PID来标识每个进程,PID是一个数,表示为pid_t隐含类型(指数据类型的物理表示是未知的或不相关的)。PID的最大值默认设置为32768(short int短整型的最大值,16位2字节),内核把每个进程的PID存放在它们分子的进程描述符中。系统管理员可以通过修改/pro/sys/kernel/pid_max来提高上限。
在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要。

进程状态

进程描述符中的state域描述了进程的当前状态,系统中的每个进程必然处于五个进程状态中的一种,state域的值也必为下列五种转台标志之一:

  • TASK_RUNNING(运行):进程是可执行的;它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的进程。
  • TASK_INTERRUPTED(可中断):进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。
  • TASK_UNINTERRUPTABLE(不可中断):这个状态通常在进程必须等待时不受干扰或等待事件很快就会发生时出现。
  • __TASK_TRACED:被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪
  • __TASK_STOPPED:进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。

设置当前进程状态

内核经常需要调整某个进程的状态,这时最好使用set_task_state(task, state)函数:

set_task_state(task, state); //将任务task的状态设置为state
//可以通过此调度进程进入执行状态

进程上下文

可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行,一般程序在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间

进程家族树

所有的进程都是PID为1的init进程的后代,所有孤儿进程也会过继到init进程的名下。内核在系统启动的最后阶段启动init进程。注意到每个task_struct中都包含一个指向其父进程task_struct的指针,还包含一个称为children的子进程链表。

进程创建

Unix系统把创建进程的步骤分解到两个单独的函数中去执行:fork()和exec()。首先fork()通过拷贝当前进程创建一个子进程。子进程和父进程的区别仅仅在于PID、PPID和某些资源和统计量。exec函数负责读取可执行文件并将其载入地址空间开始运行。

fork()

Linux通过clone()系统调用实现fork()。fork()、vfork()和__clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()。

vfork()

除了不拷贝父进程的页表项外,vfork()系统调用和fork的功能相同。子进程作为一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。

线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存空间运行的一组线程。从Linux内核的角度来说,它并没有线程这个概念,Linux所有的线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。

内核线程

内核经常需要在后台执行一些操作,这种任务可以通过内核线程完成。内核线程指的是运行在内核空间的标准线程。

进程终结

当一个进程终结时,内核必须释放它所占有的资源。进程通过自身引起的析构来终结。进程调用exit()系统调用时,该任务大部分要靠do_exit()来完成,包括释放进程的相关资源以及上下文的切换(调用schedule())。

删除进程描述符

在调用了do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的描述符。因此进程中止时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值