2020-01-17 进程管理

2.进程管理

进程是Unix操作系统最基本的抽象之一。
定义: 进程就是处于执行期的程序,以及它所包含的资源的总称

包含:
可执行程序代码 (Unix称为代码段 Text section)
存放全局变量的数据段 Data section
打开文件、挂起的信号
地址空间一个或多个执行线程 threads of execution

另一个名字:任务task,内核通常把进程也叫做任务

2.1 进程描述符及任务队列

                +-------------------------+
          +-->  |    struct task_struct   |
          +-----+-------------------+-----+
      +-> |    struct task_struct   |     |
      +---+---------------------+---+     |
+-->  |    struct task_struct   |   |     |
+-----+-------------------+-----+   |     |
|    struct task_struct   |     |   |     |
+-------------------------+     |   |     |
|  unsigned long state;   |     |   |     |
|  int prio;              |     |   |     |
|  unsigned long policy   |     |   | <---+
|  struct task_struct *parent   |   |   
|  struct list_head tasks;|     |   | 
|  pid_t pid;             |     |<--+ 
|  ...                    |     |   
|                         |     | 
|     进程描述符          | <---+
|                         |     
+-------------------------+     

|                  任务链表                  |
|                                            |
+--------------------------------------------+

2.1.1 分配进程描述符

Linux通过slab分配器分配task_struct 结构(预先分配和重复使用task_struct,避免动态分配的资源消耗,所以进程创建迅速)

             进程内核栈
+------------------------+ 最高的内存地址
|                        |
|                        |
|                        |
+------------------------+ 栈指针
|                        |
|                        |
+------------------------+
|                        |
|                        |
| struct thread_struct   |
+----+-------------------+ 最低的内存地址   current_thread_info()-
     |
     | thread info 有一个指向进程描述符的指针
     |
     |
     v-----------------> 进程的task_struct结构

进程描述符及内核栈

struct thread_info {
    struct task_struct *task;
    struct exec_domain *exec_domain;
    unsigned long flags;
    _u32 cpu
    _s32 preempt_count;
    mm_segment_t addr_limit;
    u8 supervisor_stack[0];
}

2.1.2 进程描述符的存放

PID pid_t

内核中,访问任务通常需要获得指向其task_struct指针,内核大部分处理进程的代码都是直接通过task_struct进行的

x86 寄存器并不富余,只能在内核在的尾端创建thread_info结构,通过计算偏移间接地查找task_struct

2.1.3 进程状态

   创建新进程                       任务被终止
+------------------+              +------------------+
|                  |              |                  |
|   fork()         |              | TASK_ZOMBIE      |
|   a new task     |              |                  |
+-----+------------+              +------------------+
      |
      |                                  ^
      |           调度程序将任务投入运行 |
      |           schedule()函数调用     |
      |           context_switch()       |
      |      +---------------------+     |  任务通过do_exit()函数退出
      v      |                     |     |
             |                     v     |
 +-----------+------+            +-+-----+----------+
 | TASK_RUNNING     |            | TASK_RUNNING     |
 |                  |            |                  |  正在运行
 +------------------+            +---+-------+------+
   准备就绪      ^                   |       |
   还未投入运行  |                   |       |
        ^        |                   |       |
        |        +-----<-------------+       |
        |         任务被优先级更高           |
        |         的任务抢占                 |
        |                                    |
        |                                    |为了等待特定事件
        |                                    |任务在等待队列上睡眠
        |                                    |
        |        +---------------------+     |
        |        |TASK_INTERRUPTIBLE   |     |
        +--------+or                   |     |
                 |TASK_UNINTERRUPTIBLE | <---+
等待的事件发生后 |                     |
任务被唤醒       +---------------------+
重新进入运行队列              等待

进程上下文

当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间
此时,我们称内核"代表进程执行",并处于进程上下文中。
除非在此间隙有更高优先级的进程需要执行并由调度器做出了调整,否则在内核退出时候,程序恢复在用户空间继续执行
进程只有通过系统接口(系统调用和异常) 才能陷入内核

                    +--------+      +--------+
                    |        |      |        |
         +----------+parent  | <----+parent  |
         |          |children+----> |children|
         v          +--------+      +--------+
    +--------+      +--------+      +--------+
    |        |      |        |      |        |
    | init   | <----+parent  | <----+parent  |
    |        |      |children+----> |children|
    +--------+      +--------+      +--------+

2.2进程创建

fork() 复制当前进程创建子进程
exec() 读取可执行文件,并将其载入地址空间开始运行

linux 的fork,并不会立即复制整个进程地址空间,而是让父进程和子进程共享一个拷贝
在需要写入的时候,数据才会被复制

而 fork 之后立即调用exec() ,他们就无需复制,可以避免大量复制父进程数据

普通fork()

clone(SIGCHLD,0);

vfork()

clone(CLONE_VFORK | CLONE_VM | CLONE_SIGHAND, 0);

2.3 线程在Linux 中的实现

线程提供了在同一程序内共享内存地址空间运行的一组线程
线程可以共享打开的文件和其他资源,线程机制支持并发程序设计技术,在多处理器上,它能保证真正的并行处理

linux实现比较独特。内核的角度来说,他们有线程的该你那,把所有的线程都当做进程来实现。
线程仅仅被视为一个使用某些共享资源的进程。
每个线程都拥有唯一隶属于自己的task_struct,在内核中,它看起来像是一个普通的进程
该进程和其他一些进程共享某些资源,比如地址空间

线程的创建和普通进程的创建类似,只不过在调用clone()时候需要传递一些参数标志来指明需要共享的资源

clone(CLONE_VM | COLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

共享地址,共享文件资源,共享文件描述符,共享信号

参数标记 含义
CLONE_CLEARTID 清除TID
CLONE_DETACHED 父进程不需要子进程退出时发送SIGCHLD
CLONE_FILES 父进程共享打开的文件
CLONE_FS 父子进程共享文件系统信息
CLONE_IDLETASK 将PID设置为0(只提供idle进程使用)
CLONE_NEWNS 为子进程创建新的命名空间
CLONE_PARENT 指定子进程与父进程拥有同一个父进程
CLONE_PTRACE 继续调试子进程
CLONE_SETTID 将TID回写到用户空间
CLONE_SETTLS 为子进程创建新的TLS
CLONE_SIGHAND 父子进程共享信号处理函数
CLONE_SYSVSEM 父子进程共享System V SEM_UNDO 语义
CLONE_THREAD 父子进程放入相同的线程组
CLONE_VFORK 调用vfork(),所以父进程准备睡眠等待子进程将其唤醒
CLONE_VM 父子进程共享地址空间

2.3.1 内核线程

内核需要在后台执行一些操作,这种任务可以通过内核线程 kernel thread完成
独立运行在内核空间的标准进程
内核线程和普通进的区别在于内核线程没有独立的地址空间 实际上他的 mm 指针被设置NULL

2.4 进程终结

所有进程的终止都是由do_exit()函数来处理的,这个函数从内核数据结构中删除对终止进程的大部分引用

  • 把进程task_struct的flag字段设置为PF_EXITING标志,以表示进程正在被删除。
  • 如果BSD的进程记账功能是开启的,要调用accp_process() 来输出记账信息
  • 调用_exit_mm()函数放弃进程占用的mm_struct,如果没有别的进程使用它们(如果没有别的进程使用它们,也就是说,它们没有被共享),就彻底释放它们
  • 如果需要,通过函数del_timer_sync()从动态定时器队列中删除进程描述符。
  • 分别调用exit_mm()、exit_sem()、__exit_files()、__exit_fs()、exit_namespace()和exit_thread()函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及I/O权限位图相关的数据结构。如果没有其它进程共享这些数据结构,那么这些函数还删除所有这些数据结构中。
  • 如果实现了被杀死进程的执行域和可执行格式的内核函数包含在内核模块中,则函数递减它们的使用计数器。
  • 把进程描述符的exit_code字段设置成进程的终止代号,这个值要么是_exit()或exit_group()系统调用参数,要么是由内核提供的一个错误代码。
  • 调用exit_notify()函数,向父进程发送信号,将子进程的父进程重新设置为线程组中的其他线程或init进程, 状态 TASK_ZOMBIE
  • 调用 schedule()切换到其他进程

具体exit_notify

a. 更新父进程和子进程的亲属关系。如果同一线程组中有正在运行的进程,就让终止进程所创建的所有子进程都变成同一线程组中另外一个进程的子进程,否则让它们成为init的子进程

b. 检查被终止进程其进程描述符的exit_signal字段是否不等于-1,并检查进程是否是其所属进程组的最后一个成员。在这种情况下,函数通过给正被终止进程的父进程发送一个信号,以通知父进程子进程死亡。

c. 否则,也就是exit_signal字段等于-1,或者线程组中还有其它进程,那么只要进程正在被跟踪,就向父进程发送一个SIGCHLD信号。

d. 如果进程描述符的exit_signal字段等于-1,而且进程没有被跟踪,就把进程描述符的exit_state字段置为EXIT_DEAD,然后调用release_task()回收进程的其它数据结构占用的内存,并递减进程描述符的使用计数器,以使进程描述符本身正好不会被释放。

e. 否则,如果进程描述符的exit_signal字段不等于-1,或进程正在被跟踪,就把exit_state字段置为EXIT_ZOMBIE。

f. 把进程描述符的flags字段设置为PF_DEAD标志。

调用schedule()函数选择一个新进程运行。调度程序忽略处于EXIT_ZOMBIE状态的进程,所以这种进程正好在schedule()中的宏switch_to被调用之后停止执行。

2.4.1 删除进程描述符

调用 do_exit()之后,尽管线程已经僵尸不能运行,但是系统还保留他的进程描述符
父进程获得已终结的子进程的信息后,子进程task_struct结构才能释放

wait() 这一组函数都是通过唯一的一个系统调用wait4()实现的,他的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程PID。此外,调用该函数时提供的指针会包含子函数退出是的退出代码

release_task()

  • free_uid() 减少该进程拥有者的进程使用计数。

linux用一个单用户告诉缓存统计和记录每个用户占用的进程数目、文件数目。如果这些数目都为0,表明这个用户没有使用任何进程和文件,那么这块缓存可以销毁

  • unhash_process() 从pidhash上删除该进程,同时也要从task_list中删除该进程
  • 如果这个进程正在被ptrace追踪,将跟踪进程的父进程重设为其最初的父进程并将它从ptrace list上删除
  • put_task_struct()释放进程内核栈和thread_info结构所占用的页,并释放task_struct所占用的slab高速缓存

2.4.2 孤儿进程

如果父进程在子进程之前推出,必须有机制来保证子进程能找到一个新的父亲,否则这些孤儿的进程就会在推出时候永远处于僵尸状态,白白消耗内存。

你可能感兴趣的:(2020-01-17 进程管理)