第3章 进程管理

进程是Unix操作系统抽象概念中最基本的一种,进程管理是操作系统的心脏所在。

3.1 进程

  • 进程:处于执行期的程序以及相关的资源的总称。
  • 线程:在进程中活动的对象,拥有独立的程序计数器、进程栈和一组进程寄存器。

内核调度的对象是线程而不是进程。

存在包含多个线程的多线程程序
存在两个或多个不同进程执行同一程序,并且可以共享资源

现代操作系统中,进程提供两种虚拟机制:

  • 虚拟存储器:给进程假象好像自己在独享处理器,实际是很多进程在分享一个处理器。

  • 虚拟内存:让进程觉得自己拥有整个系统的内存资源

    注意:线程之间可以共享虚拟内存,但每个都拥有自己的虚拟处理器。

创建进程

fork()系统调用:通过复制一个现有进程来创建一个全新的进程。(进程在创建它的时候开始存活)

父进程调用fork()产生子进程
在返回点:父进程恢复执行,子进程开始执行
fork()返回两次:一次回到父进程,一次回到子进程

exec():创建新的地址空间,把新的程序载入其中。

exit():退出执行,终结程序并将其占用的资源释放掉。

wait4():父进程调用它查询子进程是否终结。

3.2 进程描述符及任务结构

进程列表存放在任务队列(双向循环链表)中,每一项都是进程描述符(类型为task_struct),包含了一个进程的所有信息:

  • 它打开的文件
  • 进程的地址空间
  • 挂起的信号
  • 进程的状态
  • 其他更多信息

分配进程描述符

以前:各个进程的task_struct存放在它们内核栈的尾端,只通过栈指针就能计算出它的位置,避免使用额外的寄存器。

现在:用slab分配器动态生成task_struct,在内核栈的尾端创建一个新的结构struct thread_info,thread_info有一个指向进程描述符的指针(task域中存放指向实际task_struct的指针)

进程描述符的存放

进程唯一的标识值PID(类型pid_t,实际是int类型),PID最大值默认设置为32768(short int型最大值,最大值可更改),PID最大值也表示系统中允许同时存在的进程的最大数目。

内核访问任务的速度关键在于找到当前进程的进程描述符task_struct,通过current宏实现:

current宏针对不同硬件体系结构:
寄存器富余的直接拿出一个专门的寄存器存放指向当前进程task_struct的指针。
寄存器不富余的,在内核栈尾端创建thread_info结构,计算偏移间接找到task_struct。 x86中,current把栈指针的后13位屏蔽掉来计算thread_info结构的偏移,通过current_thread_info()函数实现:
movl $-8192,%eax //栈大小8KB,4KB->4096
andl $esp,%eax //计算偏移
current_thread_info() ->task //current从thread_info的task域中提取task_struct地址

进程状态

进程描述符stat域描述了进程当前状态,进程状态只有五种,必为其一:

TASK_RUNNING(运行):进程正在执行/在运行队列中等待执行。
TASK_INTERRUPTIBLE(可中断):进程正在睡眠(被阻塞),等待条件达成就变为执行状态,也可被信号提前唤醒。
TASK_UNINTERRUPTIBLE(不可中断):对信号不作响应,其余与可中断状态相同。
__TASK_TRACED:被其他进程跟踪的进程。
__TASK_STOPPED:进程停止执行

设置当前进程状态

set_task_state(task,state);    //将task状态设置为state
必要时会设置内存屏障来强制其他处理器作重新排序,否则等价于:task->state = state

进程上下文

可执行程序代码是进程的重要组成部分,从可执行文件载入到进程的地址空间执行。

陷入内核:内核代表进程执行,从用户空间到内核,此时内核处于进程上下文,current在此上下文中是有效的,内核退出->恢复上下文。

系统调用和异常处理程序是对内核明确定义的接口

进程家族树

进程之间存在明显的继承关系,所有进程都是init进程(PID为1)的后代,系统启动的最后阶段就是启动init进程。

每个进程必有一个父进程,也有0或多个子进程,进程间的关系存放在进程描述符中(task_struct):

parent指针:指向父进程tast_struct
children:子进程链表

init进程的进程描述符是作为init_task静态分配的。

struct tsak_struct *task;
for (task = current;task != &init_task;task = task->parent)
//一直找寻当前进程的父进程,直到找到init进程

实际上从系统的任意一个进程出发都能找到任意指定的其它进程。

获取任务队列的下一个进程:list_entry(task->tasks.next,struct task_struct,tasks)
获取任务队列的前一个进程:list_entry(task->tasks.prev,struct task_struct,tasks)
//通过next_task(task)宏和prev_task(task)宏实现 for_each_process(task)宏可依次访问整个任务队列,每次访问都指向链表中的下一个元素,但遍历所有进程代价大。

3.3 进程创建

一般产生进程的机制:

  • 在新的地址空间里创建进程
  • 读入可执行文件
  • 开始执行

UNIX中:

fork():通过拷贝当前进程来创建一个新的子进程。子父进程区别:PID(进程唯一)、PPID(父进程进程号)、某些资源统计量。
exec():读取可执行文件并将其载入新的地址空间开始运行。

写时拷贝

传统的fork()系统调用直接把所有资源复制给新进程,这样效率低。

Linux使用写时拷贝页:推迟甚至免除拷贝数据,父进程和子进程共享一个拷贝(只读),在需要写入的时候,数据才会被复制。

fork()的实际开销:

复制父进程的页表
给子进程创建唯一的进程描述符

fork()

clone()系统调用通过一系列参数标志来指明父子进程需要共享的资源

创建进程:大部分靠do_fork()

fork()、vfork()、_clone()库函数根据各自需要的参数标志去调用clone()->do_fork()完成创建的大部分工作->copy_process()函数返回指向子进程的指针->回到do_fork(),子进程被唤醒先执行。

vfork()

除了不拷贝父进程的页表项,与fork()功能相同。

子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程堵塞直到子进程退出或执行exec().

子进程不能向地址空间写入,理想情况下,最好不用vfork()。

3.4 线程在Linux中的实现

线程机制:现代编程技术中常用的一种抽象概念,提供在同一程序内共享内存地址空间运行的一组线程,还可共享打开的问价和其他资源。支持并发程序设计技术。

从内核角度来说,并没有线程的概念,都是进程,只是线程可以和其他进程共享资源,每个线程都有自己的task_struct。

创建线程

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

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

父子共享地址空间、文件系统资源、文件描述符和信号处理程序。

普通fork()实现:clone(SIGCHLD,0);
vfork()实现:clone(CLONE_VFORK | CLONE_VM | SIGCHLD,0);

传递的参数标志决定了新创建进程的行为方式和父子进程之间共享资源的种类。书上有具体参数标志的含义。

内核线程

内核线程:独立运行在内核空间的标准进程,没有独立的地址空间,只在内核空间运行,可以被调度或抢占。

ps -ef  //看到内核线程

内核线程只能由内核线程创建,都是从kthreadd内核线程中衍生出来的新内核线程。

创建新内核线程:kthread_create()  新进程处于不可运行状态
明确唤醒进程,否则不会主动运行:wake_up_process()
让线程运行起来:kthread_run()
线程自己调用退出:do_exit()
其他调用使线程退出:kthread_stop() 传递的参数为kthread_create()函数返回的task_struct结构的地址

3.5 进程终结

进程终结时:内核释放它占有的资源并告知其父进程。

1、清理工作

进程的析构:进程调用exit():

  • 显示调用
  • 隐式的从某个程序的主函数返回
  • 当进程接受到它既不能处理也不能忽略的信号或异常时,可能被动的终结。

进程终结:大部分靠do_exit()

do_exit()永不返回

释放掉与进程相关联的资源,进程不可运行并处于EXIT_ZOMBIE退出状态
占用的所有内存:内核栈、thread_info结构和tast_struct结构。
此时进程存在唯一目的就是向父进程提供信息。

2、删除进程描述符

do_exit()后,系统还保留了它的进程描述符,唯一目的就是向父进程提供信息,当父进程获得已终结的子进程的信息后或者通知内核那是无关信息后,子进程的进程描述符才被释放。

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

释放进程描述符:release_task()

释放进程内核栈、thread_info结构和tast_struct结构

孤儿进程造成的进退维谷

如果父进程在子进程之前退出,必须给子进程找到一个新的父亲,否则这些孤儿进程在退出时将永远处于僵死状态,耗费内存。

解决:寻父:

do_exit() -> exit_notify() -> forgrt_original_parent() -> find_new_reaper()
如果不行就让init做它们的父进程

遍历子进程为它们设置新的父进程:

子进程链表
子进程被跟踪时,父进程设定为调试进程,此时父进程退出了,要为它和它的兄弟新找一个父进程
ptrace子进程链表:在其中搜索相关兄弟进程

init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

20135220谈愈敏Linux Book_3的更多相关文章

  1. 20135220谈愈敏Linux Book_17

    第17章 设备与模块 关于设备驱动和设备管理的四种内核成分: 设备类型:在所有 Unix 系统中为了统一普通设备的操作所采用的分类. 模块: Linux 内核中用于按需加载和卸载目标码的机制. 内核对 ...

  2. 20135220谈愈敏Linux Book_4

    进程调度 进程:程序的运行态表现形式 进程调度程序:确保进程能有效工作的一个内核子系统,决定将哪个进程投入运行.何时运行以及运行多长时间,在可运行态进程之间分配有限的处理器时间资源. 最大限度的利用处 ...

  3. 20135220谈愈敏Linux Book_18

    第18章 调试 调试内核艰难且风险高,关键在于对内核的深刻理解. 18.1 准备开始 需要的是: 一个bug 一个藏匿bug的内核版本 相关内核代码的知识和运气 内核中的bug不是很清晰,调试成功的关 ...

  4. 20135220谈愈敏Linux Book_5

    第五章 系统调用 内核提供了用户进程与内核进行交互的一组接口. 应用程序发出请求->内核负责满足 目的:保证系统稳定可靠 5.1 与内核通信 系统调用在用户空间进程和硬件设备之间添加了一个中间层 ...

  5. 20135220谈愈敏Linux Book_1&2

    第一章 Linux内核简介 从unix的历史视角来认识Linux内核与Linux操作系统的前世今生. Unix历史 贝尔实验室设计的一个文件系统原型逐渐演化而成Unix,而后Unix操作系统用C语言重 ...

  6. 20135220谈愈敏Blog3_构造一个简单的Linux系统MenuOS

    构造一个简单的Linux系统MenuOS 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1 ...

  7. 20135220谈愈敏Blog8_进程的切换和系统的一般执行过程

    进程的切换和系统的一般执行过程 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-100002 ...

  8. 20135220谈愈敏Blog7_可执行程序的装载

    可执行程序的装载 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 一. ...

  9. 20135220谈愈敏Blog6_进程的描述和创建

    进程的描述和创建 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 进程 ...

随机推荐

  1. Gleeo Time Tracker简明使用教程

    转载一篇很不错的文章,这款软件还是非常实用的 1 简介 Gleeo Time Tracker是安卓平台下一款相当酷的项目时间记录和管理的软件.说他酷,是因为界面纯黑.而除了这点酷之外,功能也很简单实用 ...

  2. Resharper注册机实现以及源代码

    一直用Resharper,每次找注册机也麻烦,就想怎么才能自己能做个注册机,把它原理给搞懂了,不就不用找了.今天早上起来研究了下Resharper注册机,网上找到一位大神之前做Resharper注册机 ...

  3. 每日Scrum--No.5

    Yesterday:学习并编写代码 Today:组织小组开一次阶段性的总结会议:讨论需求分析中存在的问题:继续学习和编写代码:总结前阶段代码出现的问题 Problem:编程要注意很多的特殊情况,程序成 ...

  4. Effective Java 59 Avoid unnecessary use of checked exceptions

    The burden is justified if the exceptional condition cannot be prevented by proper use of the API an ...

  5. 如何在Java Filter 中注入 Service

    在项目中遇到一个问题,在 Filter中注入 Serivce失败,注入的service始终为null.如下所示: public class WeiXinFilter implements Filter ...

  6. C/S架构程序多种类服务器之间实现单点登录(转)

    (一) 在项目开发的过程中,经常会出现这样的情况:我们的产品包括很多,以QQ举例,如登陆.好友下载.群下载.网络硬盘.QQ游戏.QQ音乐等,总不能要求用户每次输入用户名.密码吧,为解决这个问题,高手提 ...

  7. 初识50个Linux命令

    1. [命令]:cat [功能说明]: concatenate files and print on the standard output #连接文件并打印到标准输出,有标准输出的都可以用重定向定向 ...

  8. 04_最长上升子序列问题(LIS)

    来源:刘汝佳<算法竞赛入门经典--训练指南> P60 问题6: 问题描述:给定n个整数a1,a2,...,an,按从左到右的顺序选出尽量多的整数,组成一个上升子序列(子序列可以理解为:删除 ...

  9. 百度地图简单使用——添加折线,圆形等(html,js)

    地图覆盖物概述 所有叠加或覆盖到地图的内容,我们统称为地图覆盖物.如标注.矢量图形元素(包括:折线和多边形和圆).信息窗口等.覆盖物拥有自己的地理坐标,当您拖动或缩放地图时,它们会相应的移动. 地图A ...

  10. linux sed命令

    一.初识sed 在部署openstack的过程中,会接触到大量的sed命令,比如 # Bind MySQL service to all network interfaces. sed -i 's/1 ...