【Linxu内核设计与实现】-第3章 进程管理

释放双眼,带上耳机,听听看~!

第三章 进程管理

主要内容:

  • 进程的定义及相关概念
  • 内核如何管理进程
  • 进程的列举、创建、消亡
  • 进程管理是操作系统的心脏

3.1 进程

进程

(1)进程就是出于执行期的程序(目标码存放在某种存储介质上)。进程不局限于一段可执行代码,还包含其他资源(如打开的文件,挂起的信号,处理器状态等等)。

(2)进程定义:处于执行期的程序及相关资源的总称。

(3)内核调度的对象:线程

(4)线程是在进程中活动的对象,每个线程都拥有独立的程序计数器,进程栈,和一组进程寄存器。

(5)Linux线程很特别:对线程和进程不特别区分,线程是特殊的进程。

fork

(6)fork()系统:该
系统调用通过复制一个现有的进程来创建一个全新的进程。

  • 父进程:调用fork的进程
  • 子进程:新产生的进程

fork调用结束后,在返回相同位置上,父进程恢复执行,子进程开始执行。

(7)fork()系统调用从内核返回两次,一次回到父进程,一次回到子进程。

(8)创建进程的目的:立即执行新的、不同的程序,接着调用
exec()
函数创建新的地址空间,并把新的程序载入其中。

(9)fork()实际上是由clone()系统调用实现的。

(10)退出:通过exit()系统调用退出执行,该函数会终止进程并将占用的资源释放掉。

(11)父进程可以通过wait4()系统调用查看子进程是否终止,使进程拥有了等待特定进程执行完毕的能力。(shell进程中执行应用程序)

(12)进程退出执行后,处于僵死状态,直到它的父进程调用wait()或者waitpid()为止。

(13)别名:进程又称为任务。

3.2

进程描述符及任务结构

    操作系统运行时会有很多进程,内核将多个进程存放在一个双向循环链表中,通过PCB来描述一个进程的所有信息,即进程描述符。它是一个结构体,里面
包含了一个进程的所有信息。如进程的状态,打开的文件,父进程等等。

3.2.1 分配进程描述符

Linux通过slab分配器来分配task_struct结构。在栈底或栈顶创建一个struct thread_info结构体。其中包含PCB的指针。

3.2.2 进程描述符的存放

(1)    内核通过PID来唯一的标识每个进程,pid是一个int数,最大值默认为32768(short),可以修改配置文件来调整,pid存放在进程各自的PCB中。

(2)    访问任务需要获取指向其task_struct的指针

(3)    通过current宏可以查看当前正在运行进程的进程描述符

(4)    指向当前进程task_struct的指针存放在:

  • 专门的寄存器
  • 栈尾的thread_info结构体中

3.2.3进程的状态

进程描述符PCB中的state域描述了进程当前的状态。
每个进程必然处于5种状态之一。

3.2.4 设置当前进程的状态

set_task_state(task,state)函数将指定的进程设置成指定的状态。

3.2.5 进程上下文

(1)可执行文件被载入到进程的地址空间,一般为用户空间。当程序执行了系统调用或者触发了某个异常,就陷入内核空间。此时我们称处于进程上下文中。

(2)
系统调用,异常处理程序 :是对内核明确定义的接口,程序只有通过这些接口才能陷入内核执行,对内核所有访问都必须通过这些接口。

3.2.6 进程家族树

(1)
所有进程都是PID为1的init进程的后代。

(2)内核在系统启动的最后阶段启动
init进程,该进程读取系统的初始化脚本并执行其他相关程序,最终完成系统启动的整个过程。

(3)每个进程必有一个父进程,每个进程也可以拥有0个或对个子进程。拥有同一个父进程的所有进程称为兄弟。进程间的关系存放在进程描述符中。

(4)每一个task_struct都包含一个指向父进程的task_struct,叫做partent的指针。还包含一个称为children的子进程链表。

(5)任务队列是循环双向链表,可以通过任一进程获取任一进程。

3.3 进程创建

(1)其他操作系统:在新地址空间里创建进程,读入可执行文件,最后开始执行。

(2)Unix分开成了两部:fork(),exec();

fork():拷贝当前进程创建一个子进程,子父进程的区别仅在于PID,PPID和某些资源和统计量(如挂起的信号,子进程没必要继承)。

exec():读取可执行文件,并将其载入地址空间,并开始运行。

3.3.1 写时拷贝

(1)传统fork():直接把所有资源复制给新创建的进程,简单但效率低

(2)linux-fork():通过写时拷贝页实现

(3)
写时拷贝:推迟甚至免除拷贝的一种技术,内核此时并不复制整个进程地址空间,而是让父子进程共享一个拷贝。只有在需要的时候,数据才会被复制,在此之前是以只读的方式共享。

(4)Unix强调进程的快速执行能力。

(5)进程创建后往往会马上执行一个可执行程序,这种优化可以避免拷贝大量根本不会被使用的数据,提高了进程的执行效率。

3.3.2 fork()

(1)Linux通过clone()实现fork(),这个调用通过一系列参数标志来指明父子进程需要共享的资源。

(2)fork(),vfork(),__clone()都根据自己需要的参数标志去调用clone(),然后由clone()调用do_fork()。

(3)do_fork()完成创建进程中的大部分工作,在kernel/fork.c中定义。该函数调用copy_process()函数,然后让进程开始运行。

(4)copy_process()函数:

3.3.3 vfork()

 

3.4 线程在Liuux中的实现

(1)线程和进程一样都是一种抽象概念

(2)线程机制提供了在同一程序内共享内存地址空间运行的一组线程,这些线程可以共享打开的文件和其他资源。

(3)由于线程的存在,在多处理器上可以实现真正的并行处理。

(4)Linux把所有线程都当做普通的进程来看,线程被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一属于自己的task_struct。只是线程与其他一些进程共享某些资源,如地址空间。

(5)对于Linux来说,线程只是一种进程间共享资源的一种手段。

3.4.1 创建线程

(1)通过clone()来创建进程,线程也一样通过clone(),但是在调用的时候传入一些标志来指明需要共享的资源:

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

上面与调用fork()差不多,只是父子俩共享地址空间,文件系统资源,文件描述符和信号处理程序。

对比一下,一个普通fork()的实现是:

Clone(SIGCHLD,0);

(2)传递给clone()的参数标志,决定了新创建进程的行为方式和父子进程之间共享的资源种类。如下表:

3.4.2 内核线程

(1)内核线程:独立运行在内核空间的标准进程

(2)可以被调度和抢占

(3)ps –ef:查看内核线程

(4)内核线程的创建

3.5 内核终结

(1)当一个进程终结时,内核必须释放它所占有的资源,并通知父进程。

(2)最终通过do_exit()来终结进程。其工作为:

3.5.1 删除进程描述符

3.5.2 孤儿进程造成的进退维谷

补充学习:

(1)由进程的创建,来思考
Linux shell中应用程序执行流程?

执行应用程序的方式有很多,从shell中执行是一种常见的情况。交互式shell是一个进程(所有的进程都由pid号为1的init进程fork得到),当在用户在shell中敲入./test执行程序时:

父进程行为:

  • shell先fork()出一个子进程(这也是很多文章中说的子shell)
  • wait()这个子进程结束
  • 当test执行结束后,又回到了shell等待用户输入(如果创建的是所谓的后台进程,shell则不会等待子进程结束,而直接继续往下执行)。
  • 所以shell进程的主要工作是复制一个新的进程,并等待它的结束。

子进程行为:

  • 调用execve()加载test并开始执行。这是test被执行的关键。
  • 子进程通过execve系统调用启动加载器。加载器删除子进程已有的虚拟存储段,并创建一组新的代码、数据、堆、栈段,新的堆和栈被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小组块,新的代码和数据段被初始化为可执行文件的内容,最后将CUP指令寄存器设置成可执行文件入口,启动运行。

execve()是操作系统提供的非常重要的一个系统调用,在很多文章中被称为exec()系统调用(注意和shell内部exec命令不一样),其实在Linux中并没有exec()这个系统调用,exec只是用来描述一组函数,它们都以exec开头,分别是:

int execl(const char *path, const char*arg, …);

int execlp(const char *file, const char*arg, …);

int execle(const char *path, const char*arg, …, char *const envp[]);

int execv(const char *path, char *constargv[]);

int execvp(const char *file, char *constargv[]);

int execve(const char *path, char *constargv[], char *const envp[]);

库函数exec*都是execve的封装例程。

(2)系统调用

(3)exec()函数

(4)init进程

给TA打赏
共{{data.count}}人
人已打赏
安全运维

WordPress网站专用docker容器环境带Waf

2020-7-18 20:04:44

安全运维

运维安全-Gitlab管理员权限安全思考

2021-9-19 9:16:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索