linux第三章学习笔记

第三章 进程管理

进程是Unix操作系统抽象概念中最基本的一种。

进程管理是所有操作系统的心脏所在。

一、进程

1. 进程是处于执行期的程序。除了可执行程序代码,还包括打开的文件、挂起的信号、内核内部数据、一个或者多个执行线程等多种资源

  • 线程是在进程活动中的对象;内核调度的对象是线程而不是进程
  • 在Linux系统中,并不区分线程和进程
  • 可能存在两个或者多个进程执行的是同一个程序;甚至N个进程共享打开的文件、地址空间之类的资源

2. 线程:是进程中活动的对象。每个线程都有一个独立的程序计数器,进程栈和一组进程寄存器。内核调度的对象是线程。

3. 在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。同一进程中的线程之间可以共享虚拟内存,但是每个都拥有自己的虚拟存储器

4. 进程的生命周期

  • fork():创建新进程
  • exec():创建新的地址空间并把新的程序载入其中
  • clone():fork实际由clone实现
  • exit():退出执行
  • wait4():父进程查询子进程是否终结
  • wait()、waitpid():程序退出执行后变为僵死状态,调用这两个消灭掉。

5. 进程在创建它的时刻开始存活,这通常是调用fork系统的结果。该系统调用通过复制一个现有进程来创建一个全新的进程。fork系统调用从内核返回两次,一次到父进程,另一次回到新产生的子进程。

二、进程描述符及任务结构

进程描述符:进程列表存放在任务队列(task list)这一双向链表中,链表的项是task_struct即进程描述符的结构。该类型定义在<linux/sched.h>中。进程描述符包含的数据能完整地描述一个正在执行的程序:

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

2.1 进程描述符的分配

  • 目的:Linux通过slab分配task_ struct结构,以达到对象复用以及和缓存着色的目的,避免资源动态分配和释放带来的资源消耗
  • 现在用slab分配器动态生成task_ struct,所以只需要在栈底(对向下增长的栈来说)或栈顶(对向上增长的栈来说)创建一个新的结构struct thread_ info

每个任务的thread_info结构在它的内核栈的尾端分配。每个任务的堆栈尾端(比如,对于向上增长的堆栈来说,就是在堆栈的栈顶)有结构体thread_ info,它指向了task_ struct结构体

2.2 进程描述符的存放

1. 内核中的大部分处理处理进程的代码都是通过task_ struct进行的;因此,需要通过current宏查找到当前正在运行进程的进程描述符

2. 通过current宏查找到当前正在运行进程的进程描述符。X86系统中,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_ info的偏移(通过current_ thread_ info函数)

movl $-8192, %eax

andl %esp,%eax3.进程状态

3.内核通过一个唯一的进程标识值PID来标识每个进程。pid存放在各自进程描述符中。

2.3 进程状态

进程在任何时刻,都必定处于五种状态中的一种

  • TASK_RUNNING 运行
  • TASK_INTERRUPT 可中断
  • TASK_UNINTERRUPT 不可中断
  • TASK_TRACED 被其他进程跟踪的进程
  • TASK_STOPPED 进程停止运行

  • TASK_RUNNING:可能是正在运行,也可能表示可执行
  • TASK_ INTERRUPT/TASK_UNINTERRUPT:都表示正在阻塞;然而后者表示的状态收到信号之后也不会被唤醒

2.4 设置进程当前状态

调用set_ task_ state(task,state)函数将进程设置为指定状态

2.5 进程上下文

  • 可执行代码从一个可执行文件载入到进程的地址空间执行。当一个程序执行了系统调用,内核就会“代表进程执行”并处于进程上下文中
  • 对比:在中断上下文中,系统不代表进程执行——不会有进程去干扰这些中断处理程序

2.6 进程家族树

所有的进程都是pid为1的init进程的后代。
内核在系统启动的最后阶段启动init进程。

系统中的每一个进程必有一个父进程,可以拥有0个或多个子进程,拥有同一个父进程的进程叫做兄弟。
这种关系存放在进程描述符中,parent指针指向父进程task_struct,children是子进程链表。

获得父进程的进程描述符:

struct task_struct *my_parent = current->parent;

访问子进程:

struct task_struct *task;

struct list_head *list;

list_for_each(list, ¤t->children){

task = list_entry(list, struct task_struct, sibling);

/* task现在指向当前的某个子进程 */

}

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

获取链表中的下一个进程:

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)宏,依次访问整个任务队列,每次访问任务指针都指向链表中的下一个元素。

struct task_struct *task;

for_each_process(task){

/* 它打印出每一个任务的名称和PID */

printk("%s[%d]\n",task->comm, task->pid);

}

三、进程创建

Unix系统的进程创建方式

  • fork()通过拷贝当前进程创建一个子进程
  • exec()负责读取可执行文件并将其载入地址空间开始运行
  • Linux的fork()使用写时拷贝推迟甚至免除拷贝。内核在创建新进程的时候并不复制整个地址空间,而是让父进程和子进程共享同一个拷贝;直到子进程/父进程需要写入的时候才进行拷贝,在此之前以只读方式读取.
  • fork的实际开销只是复制父进程的页表以及给子进程创建唯一的进程描述符

3.1 写时拷贝

3.2 fork()

  1. Linux通过clone系统调用实现fork
  2. fork()、vfork()、__clone()都根据各自需要的参数标志调用clone()。由clone去调用do_fork()
  3. 定义在<kernel/fork.c>中的do_ fork()完成创建中的大部分工作,它调用copy_process函数,然后让进程开始运行

※一般内核会选择子进程首先执行。
why?
一般子进程会马上调用exec()函数,避免写时拷贝的额外开销。

  最后copy_process返回的就是指向子进程的指针

3.3 vfork()

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

  vfork()系统调用的实现是通过向clone()系统调用传递一个特殊标志来进行的:

  1. 调用copy_process()是,task_struct的vfor_done成员被设置为NULL。
  2. 执行do_fork()时,如果给定特定标志,则vfor_done会指向一个特定地址。
  3. 子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,知道子进程通过vfor_done指针向它发送信号。
  4. 在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfor_done是否为空,如果不为空,则会向父进程发送信号。
  5. 回到do_fork(),父进程醒来并返回。

四、线程在Linux中的实现

  线程机制提供了在同一程序内共享内存地址空间运行的一组线程。在Linux系统中,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都有自己的task_struct。

4.1 创建线程

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

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

4.2 内核线程

内核线程与普通进程的区别只在于内核线程没有独立的地址空间,从来不切换到用户空间,可以被调度和被抢占.

  • 它只能通过其他内核线程创建;内核通过kthread内核进程衍生所有的内核线程
  • 新创建的线程处于不可运行状态,直到wake_ up_process()明确地唤醒它

五、进程终结

终结进程大部分依赖于do_exit()来完成:

给子进程重新找养父(线程组中的其他线程或者init进程)

调用schedule()切换到新的进程

这之后,进程不可运行并处于EXIT_ZONBIE退出状态,占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。

5.1 删除进程描述符

  1. 该任务是和清理工作分开进行的,因为这样在进程终结之后系统仍然可以获得它的信息
  2. 通过release_task()实现进程描述符的删除
  3. 至此,所有资源都被释放了

5.2 解决孤儿进程

1. 孤儿进程:父进程在进程之前退出,就会遗留下子进程,也就是孤儿进程

2. 解决方法:在当前的线程组内给孤儿进程寻找新的父进程;

否则直接以init作为其父进程

  • 调用顺序:do_ exit()-->forget_ original_ parent()-->find_ new_ parent()-->ptrace_ exit_ finish()

do_exit()中会调用exit_notify()
exit_notify()会调用forget_original_parent()
forget_original_parent()会调用find_new_reaper()
然后遍历所有子进程并为它们设置新的父进程。
然后调用ptrace_exit_finish()同样进行新的寻父过程,是给ptraced的子进程寻找父亲。
最后init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

一旦系统为进程成功地找到和设置了新的父进程,就不会再有出现驻留僵死进程的危险了。init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

时间: 2024-10-04 05:43:46

linux第三章学习笔记的相关文章

《Linux命令行与shell脚本编程大全》 第三章 学习笔记

第三章:基本的bash shell命令 bash程序使用命令行参数来修改所启动shell的类型 参数 描述 -c string 从string中读取命令并处理他们 -r 启动限制性shell,限制用户在默认目录下活动 -i 启动交互性shell,允许用户输入 -s 从标准输入读取命令 环境变量PS1.PS2 PS1:控制默认命令行提示符格式 PS2:控制后续命令行提示符格式   bash shell提示符字符串中使用的特殊字符 字符 描述 \a 报警字符 \d “日 月 年”格式显示的日期 \e

《Linux内核设计与实现》第三章学习笔记

第三章  进程管理 姓名:王玮怡  学号:20135116 一.进程 1.进程的含义 进程是处于执行期的程序以及相关资源的总称,程序本身并不是进程,实际上就是正在执行的代码的实时结果.Linux内核通常把进程也叫“任务”. 2.线程的含义 执行线程简称线程,是在进程中互动的对象.内核调度的对象是线程而不是进程.Linux系统不区分线程和进程,线程只是一种特殊的进程. 3.进程的执行过程 (1)clone()调用fork(),通过复制一个现有进程来创建一个全新的进程,进程开始存活.其中调用fork

《程序员的自我修养》第三章学习笔记

1,  编译器编译源代码生成的文件叫做目标文件. 从结构上说,是编译后的可执行文件,只不过还没有经过链接 3.1 目标文件的格式 1,可执行文件的格式: Windows下的PE  和   Linux下的ELF 2,从广义上说,目标文件与可执行文件的格式几乎是一样的,所以广义上可以将目标文件与可执行文件看成是一种类型的文件. 3,可执行文件,动态链接库,静态链接库都按照可执行文件格式存储(Windows下是 PE-COFF格式,Linux下是ELF格式). 4,Linux下命令: $: file 

《Java从入门到精通》第三章学习笔记

第3章 Java语言基础 一.标识符和关键字 1.Java中的标识符用于标识类名.变量名.方法名.数组名.文件名. 2.标识符的命名规则:由字母.数字.下划线和美元符号组成,但第一个字符不能是数字.同时标识符不能选用Java的关键字和保留字. 3.一般的约定命名规则: (1)类和接口名:每个词首字母大写,如MyClass,HelloWorld: (2)方法名:第一个词的首字母小写,其余词首字母大写,尽量少用下划线,如myMethod,myGetData: (3)常量名:基本数据类型的常量名全部用

第三章学习笔记

一.进程 1.进程就是出于执行期的程序 2.执行线程,简称线程,是在进程中活动的对象 3.进程提供两种虚拟机制:虚拟处理器和虚拟内存 二.进程描述符和任务结构 1.内核把进程的列表存放在焦作任务队列的双向循环链表中,其中每一项都是类型为task_struct.称为进程描述符的结构 2.进程描述符的分配和存放: (1)目的:Linux通过slab分配task_struct结构,以达到对象复用以及和缓存着色的目的(避免资源动态分配和释放带来的资源消耗) (2)分配:每个任务的堆栈尾端(比如,对于向上

《从问题到程序》第三章学习笔记

知识总结 pan_area(3.24)是函数调用表达式,表示用实参3.24去调用函数pan_area.double pan_area(double r)表示本函数的名字是pan_area,其返回值类型是double.这个函数只有一个参数,所以参数表里只有一对类型描述和参数名,说明参数的类型是double,名字是r. int main() { ...... return 0; } 以main为名字的函数的地位特殊,它表示这个程序的执行起点和整个过程.在一个C程序启动时,其执行就从它的main函数的

Scala第三章学习笔记

换行后的左大括号造成的问题: class FooHolder { def foo() { println("foo was called") } } Scala认为def foo()这行代码定义了一个抽象方法.这是因为它没有捕捉到后面的大括号,认定def foo()是完整的一行语句.当编译时,它认为这是一个洗呢匿名代码块,应该在类构建过程中执行. 解决办法:加一条新的编码规定,要求所有的方法定义使用"="语法. trait FooHolder2{ def foo()

《Linux内核设计与实现》第一、二章学习笔记

<Linux内核设计与实现>第一.二章学习笔记 姓名:王玮怡  学号:20135116 第一章 Linux内核简介 一.关于Unix ——一个支持抢占式多任务.多线程.虚拟内存.换页.动态链接和TCP/IP网络的现代化操作系统 1.主要发展过程   1969年,贝尔实验室的程序员们设计了一个文件系统原型,最终发展演化成了Unix 1971年,Unix被移植到PDP-11型机中 1973年,整个Unix系统使用C语言进行重写,为后来Unix系统的广泛移植铺平了道路 Unix第六版(V6)被贝尔实

Android深度探索——第三章读书笔记及心得

了解Git ——第三章读书笔记及心得 对于Android的理解是从这学期才开始,所以不可谓说是了解的太少太少.对于Linux虽然经过了一学期的学习.经过一次紧张的实训,但是了解的也不是很多.不过我终究是知道Android和Linux是开源的,这是很多老师都曾经告诉我们的.虽然Git并不是学习Android和Linux开发必须掌握的技术,但是对于想要认真学习好这门技术的我们来说应该要努力掌握好这门技术.就像书上说的学习新技术的方式不是一味的读书,只会纸上谈兵.更应该深入的理解自己感兴趣的源代码,通