Linux内核设计与实现——第3章:进程管理

  • 进程概念
    • 程序/进程/线程
    • fork()+exec()+exit()
  • 进程的数据结构
    • 进程描述符task_struct
    • thread_info,tast_struct,stack三者关系
    • pid和tgid
    • 进程状态:五种互斥状态
    • 表示进程亲属关系的成员
  • 进程创建
    • 写时复制优化 CopyOnWrite
    • fork() / 写时拷贝优化 / vfork() 三者区别
    • fork()函数
  • 线程在Linux中的实现
  • 进程终结
    • exit()函数

3.1 进程

(1)概念

  • 程序:代码段
  • 进程:不仅限于执行期的程序,还包括其他资源,如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间,一个或多个执行线程
  • 线程:传统Unix中,一个进程包含一个线程。对Linux而言,线程只不过是一种特殊的进程罢了

(2)Linux系统进程关键函数

  • fork():由系统调用fork()创建进程,从内核返回两次:一次回到父进程,一次回到新产生的子进程,底层是由系统调用clone()实现的
  • exec():紧跟着fork(),用来创建新的地址空间,并把程序载入其中
  • exit():终结进程并将其占用的资源释放掉

3.2 进程描述符及任务结构

(1)概念

  • 任务队列(task list):存放进程的列表,双向循环链表
  • 进程描述符(process descriptor):描述一个进程的所有信息,具体的结构体为task_struct,定义在<linux/sched.h>中

  • slab分配器:Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(见12章)的目的
  • 线程描述符thread_info:与进程描述符相关的小数据结构,指向tast_struct,存放在内核栈stack中,定义在<asm/thread_info.h>中
  • 内核栈stack:每个任务的thread_info结构在stack的尾端分配

(2)thread_info,tast_struct,stack三者关系

  • thread_info存储在stack中,thread_info结构中存在一个struct task_struct的指针。因为task_struct结构从1.0到现在5.0内核此结构一直在增大。如果将此结构放在内核栈中则很浪费内核栈的空间,则在threadinfo结构中有一个task_struct的指针就可以避免
struct thread_info {
    unsigned long  flags;        /* low level flags */
    mm_segment_t  addr_limit;    /* address limit */
    struct task_struct  *task;        /* main task structure */
    int  preempt_count;    /* 0 => preemptable, <0 => bug */
    int  cpu;        /* cpu */
};
  • task_struct结构体重有一个stack的结构,此stack指针就是指向内核栈的指针
struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags;    /* per process flags, defined below */
    unsigned int ptrace;
 
#ifdef CONFIG_SMP
    struct llist_node wake_entry;
    int on_cpu;
    unsigned int wakee_flips;
    unsigned long wakee_flip_decay_ts;
    struct task_struct *last_wakee;
 
    int wake_cpu;
#endif
    int on_rq;
    ......
  • thread_union:该联合体的作用就是thread_info和stack共用一块内存区域
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

(3)进程标识符PID

struct task_struct {
    pid_t pid;  
    pid_t tgid;  
    ...
}
  • PID:Unix通过唯一的进程标识符PID来识别每个进程。
  • 备注:在CONFIG_BASE_SMALL配置为0的情况下,PID的取值范围是0到32767,即系统中的进程数最大为32768个(short int短整型的最大值)。这个值越小,转一圈越快
  • tgid和pid的区别:进程和线程组
    • tgid:用于判断这个线程属于哪个进程,一个进程就是一个线程组,所以每个进程的所有线程都有着相同的tgid
    • pid:唯一的,每个线程用有着唯一的pid
  • Reference

(4)进程状态

struct task_struct {
    volatile long state;
    ...
}
  • 五种互斥状态
状态描述
TASK_RUNNING(运行)表示进程要么正在执行,要么正要准备执行(已经就绪),正在等待cpu时间片的调度
TASK_INTERRUPTIBLE(可中断)进程因为等待一些条件而被挂起(阻塞)而所处的状态。这些条件主要包括:硬中断、资源、一些信号……,一旦等待的条件成立,进程就会从该状态(阻塞)迅速转化成为就绪状态TASK_RUNNING
TASK_UNINTERRUPTIBLE(不可中断)意义与TASK_INTERRUPTIBLE类似,除了不能通过接受一个信号来唤醒以外,对于处于TASK_UNINTERRUPIBLE状态的进程,哪怕我们传递一个信号或者有一个外部中断都不能唤醒他们。只有它所等待的资源可用的时候,他才会被唤醒。这个标志很少用,但是并不代表没有任何用处,其实他的作用非常大,特别是对于驱动刺探相关的硬件过程很重要,这个刺探过程不能被一些其他的东西给中断,否则就会让进城进入不可预测的状态
TASK_STOPPED(停止)进程被停止执行,当进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后就会进入该状态
TASK_TRACED表示进程被debugger等进程监视,进程执行被调试程序所停止,当一个进程被另外的进程所监视,每一个信号都会让进城进入该状态

(5)表示进程亲属关系的成员

  • Linux中所有的进程都是 PID=1 的 init 进程的后代
  • 每个进程都必须有一个父进程;同样的,每个进程拥有零个或多个子进程,拥有同一个父进程的所有进程称为兄弟
/*
 * pointers to (original) parent process, youngest child, younger sibling,
 * older sibling, respectively.  (p->father can be replaced with
 * p->real_parent->pid)
 */
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
 * children/sibling forms the list of my natural children
 */
struct list_head children;      /* list of my children */
struct list_head sibling;       /* linkage in my parent's children list */
struct task_struct *group_leader;       /* threadgroup leader */
字段描述
real_parent指向其父进程,如果创建它的父进程不再存在,则指向PID为1的init进程
parent指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与real_parent相同
children表示链表的头部,链表中的所有元素都是它的子进程
sibling用于把当前进程插入到兄弟链表中
group_leader指向其所在进程组的领头进程

3.3 进程创建

(1)概述

Linux的进程创建分为两个步骤:fork()和exec()

  • fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID(每个进程唯一),PPID(父进程的PID),某些字段和统计量
  • exec()负责读取可执行文件并将其载入地址空间开始i运行

(2)Copy-On-Write 写时拷贝技术

  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程
  • fork()的实际开销是为子进程开辟新的虚拟空间:复制父进程的页表,给子进程创建唯一的进程描述符
  • 如果无需写入(如fork()后立即调用exec()),则不进行复制,父进程和子进程共享同一个物理地址(内存区)
  • 如果需要更改写入,则推迟到实际发生写入时再进行复制到新的物理地址
  • 概况总结的说
    • 如果只是fork()后立马exec(),子进程无需更改父进程内容,利用CopyOnWrite优化技术:子进程只需开辟新的虚拟地址,而物理地址指向父进程
    • 如果子进程需要进行更改:子进程开辟新的虚拟地址和物理地址

(3)fork() / 写时拷贝优化 / vfork() 三者区别

  • fork():  传统的fork()系统调用直接把所有的资源复制给新创建的进程,效率低下。Linux的fork()使用写时拷贝(copy-on-write)页实现
  • 写时拷贝:Linux对fork()的优化技术,见上面(2)
  • vfork():远古时期还没有出现写时拷贝的做法,比些时拷贝做得更彻底。连页表项也不复制,虚拟地址和物理地址全部用父进程的。此时父进程被阻塞,直到子进程退出或执行exec()。可以理解为子进程作为父进程的一个线程在它的地址空间里运行
  • 详细的Reference可以看这一篇:https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

(4)fork()函数详解


3.4 线程在Linux中的实现

(1)概念

  • 从Linux内核角度而言,并没有线程这个概念。Linux的线程只是普通的进程,拥有属于自己的task_struct,但它们会与其他进程共享某些资源,如地址空间
  • Windows或者 Sun Solaris等操作系统提供了专门支持线程的机制

3.5 进程终结

(1)exit()函数

  • 待更新

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值