第三章 进程管理

一、进程

1.进程

进程就是处于执行期的程序。

进程就是正在执行的程序代码的实时结果。

进程是处于执行期的程序以及相关的资源的总称。

进程包括代码段和其他资源。

2.线程

执行线程,简称线程,是在进程中活动的对象。

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

Linux对线程并不特别区分,视其为特殊的进程。

3.虚拟处理器和虚拟内存

在现代操作系统中,进程提供两种虚拟机制:虚拟处理器虚拟内存

包含在同一个进程中的线程可以共享虚拟内存,但是每个都拥有各自的虚拟处理器。

4.几个函数

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

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

内核把进程的列表存放在叫做任务队列的双向循环链表中。

链表中的每一项都是进程描述符。

进程描述符的类型为task_struct,里面包含的数据有:

1.分配进程描述符

Linux通过slab分配器分配task_struct结构——能达到对象复用缓存着色的目的。

slab分配器——动态生成,只需在栈底或者栈顶创建一个新的结构struct thread_info。



每个任务的thread_info结构在它的内核栈的尾端分配。

结构中task域中存放的是指向该任务实际task_struct的指针。

2.进程描述符的存放

内核通过一个唯一的进程标识值PID来标识每个进程。

pid类型为pid_t,实际上就是一个int类型,最大值默认设置为32768,上限私改/proc/sys/kernel/pid_max。

pid存放在各自进程描述符中。

怎么找到进程描述符?

通过current宏查找到当前正在运行进程的进程描述符。

x86中,在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。

current通过current_thread_info()把栈指针的后13个有效位屏蔽掉,再从thread_info的task域中提取并返回task_struct的地址。

3.进程状态

进程描述符中的state域是用来描述进程当前状态的。共有五种状态,标志如下:

  • TASK_RUNNING(运行):进程是可执行的,或者正在执行,或者在运行队列中等待执行
  • TASK_INTERRUPTIBLE(可中断):进程正在睡眠/被阻塞
  • TASK_UNINTERRUPTIBLE(不可中断):睡眠/被阻塞进程不被信号唤醒
  • TASK_TRACED:被其他进程跟踪的进程
  • TASK_STOPPED(停止):进程停止执行;进程没有投入运行也不能投入运行。

    接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号时,或者调试时收到任何信号,都可以进入这种状态。

4.设置当前进程状态

用set_task_state(task,state)函数。

set_task_state(task,state); //将任务task的状态设置为state

set_current_state(state)
和下面等价
set_task_state(current,state)

5.进程上下文

程序执行系统调用或者触发异常后,会陷入内核空间,这时候内核代表进程执行,并且处于进程上下文中。

进程对内核的访问必须通过接口:系统调用和异常处理程序

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, &current->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);
}

三、进程创建

一般操作系统产生进程的机制:

1. 在新的地址空间创建进程
2. 读入可执行文件
3. 执行

Unix的机制:

fork()和exec()。

  • fork():

    通过拷贝当前进程创建一个子进程。

    子进程与父进程的区别仅在于PID,PPID和某些资源和统计量

  • exec():

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

1.写时拷贝

写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。

资源的复制只有在需要写入时才会进行,在此之前以只读方式读取。

fork的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

2.fork()

Linux通过clone()系统调用实现fork()。

创建进程的大概步骤如下:

  1. fork()、vfork()、__clone()都根据各自需要的参数标志调用clone()。
  2. 由clone()去调用do_fork()。
  3. do_fork()调用copy_process()函数,然后让进程开始运行。
  4. 返回do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

※一般内核会选择子进程首先执行

why?

一般子进程会马上调用exec()函数,避免写时拷贝的额外开销。

3.vfork()

除了不拷贝父进程的页表项之外,vfork()系统调用和fork()的功能相同。理想情况下不要调用vfork()

子进程作为父进程的一个单独的线程在它的地址空间里运行 ,父进程被阻塞,直到子进程退出或执行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内核的角度来看并没有线程这个概念,它把所有线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。

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

1.创建线程

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

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
共享地址空间、文件系统资源、文件描述符和信号处理程序。

而普通的fork:

clone(SIGCHLD, 0);

vfork():

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

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



2.内核线程

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

内核线程没有独立的地址空间只在内核空间运行,从来不切换到用户空间,可以被调度被抢占

内核线程只能由其他内核线程创建:

kthread_create()通过clone()系统调用创建内核线程后,处于不可运行状态,如果不通过wake_up_process()明确唤醒它,不会主动运行。

创建一个进程并让它运行起来 ,可以调用kthread_run()。

内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数为kthread_create()函数返回的task_struct结构的地址。

五、进程终结

进程终结时,内核必须释放它所占有的资源并告知父进程。

进程终结的原因:一般是来自自身,发生在调用exit()系统调用时。

  • 显式的调用
  • 隐式的从某个程序的主函数返回

大部分依赖于do_exit()来完成。其中有几个重点:

……
给子进程重新找养父(线程组中的其他线程或者init进程)
调用schedule()切换到新的进程
……

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

1.删除进程描述符

释放task_struct结构发生在父进程获得已终结的子进程信息并且通知内核不关注后,需要的系统调用是wait4():

挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID。

释放进程描述符时,需要调用release_task()

2.孤儿进程

如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态。

解决方法如下——

  1. 在当前进程组找一个线程作为养父
  2. 让init成为它们的父进程。

具体实现:

do_exit()中会调用exit_notify()

exit_notify()会调用forget_original_parent()

forget_original_parent()会调用find_new_reaper()

然后遍历所有子进程并为它们设置新的父进程。

然后调用ptrace_exit_finish()同样进行新的寻父过程,是给ptraced的子进程寻找父亲。

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

20135202闫佳歆--week6 课本第三章学习笔记的更多相关文章

  1. 20135202闫佳歆--week4 课本第5章学习笔记

    第五章 系统调用 一.与内核通信 系统调用在用户控件进程和硬件设备之间添加了一个中间层,作用如下" 为用户空间提供了一种硬件的抽象接口 系统调用保证了系统的稳定和安全 每个进程都运行在虚拟系 ...

  2. 20135202闫佳歆--week5 系统调用(下)--学习笔记

    此为个人笔记存档 week 5 系统调用(下) 一.给MenuOS增加time和time-asm命令 这里老师示范的时候是已经做好的了: rm menu -rf 强制删除 git clone http ...

  3. 20135202闫佳歆--week4 系统调用(上)--学习笔记

    此为个人笔记存档 week 4 系统调用(上) 一.用户态.内核态和中断处理过程 用户通过库函数与系统调用联系起来. 1.内核态 在高执行级别下,代码可以执行特权指令,访问任意的物理地址. 2.用户态 ...

  4. 20135202闫佳歆--week6 分析Linux内核创建一个新进程的过程——实验及总结

    week 6 实验:分析Linux内核创建一个新进程的过程 1.使用gdb跟踪创建新进程的过程 准备工作: rm menu -rf git clone https://github.com/mengn ...

  5. 20135202闫佳歆--week6 进程的描述与创建--学习笔记

    此为个人学习笔记存档! week 6 进程的描述与创建 一.进程的描述 1.进程控制块task_struct 以下内容来自视频课件,存档在此. 为了管理进程,内核必须对每个进程进行清晰的描述,进程描述 ...

  6. 20135202闫佳歆--week3 课本1-2章学习笔记

    第一章 Linux内核简介 一.Unix Unix是一个强大.健壮和稳定的操作系统. 简洁 绝大部分东西都被当做文件对待.这种抽象使对数据和对设备的操作都是通过一套相同的系统调用借口来进行的:open ...

  7. 20135202闫佳歆--week5 课本18章学习笔记

    第十八章 调试 内核级开发的调试工作远比用户级开发艰难的多. 一.准备开始 准备工作需要的是: 一个bug 一个藏匿bug的内核版本 相关内核代码的知识和运气 在这一章里,调试的主要思想是让bug重现 ...

  8. 20135202闫佳歆--week 7 Linux内核如何装载和启动一个可执行程序--实验及总结

    week 7 实验:Linux内核如何装载和启动一个可执行程序 1.环境搭建: rm menu -rf git clone https://github.com/megnning/menu.git c ...

  9. 《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳

    <Linux内核设计与实现>课本第三章自学笔记 进程管理 By20135203齐岳 进程 进程:处于执行期的程序.包括代码段和打开的文件.挂起的信号.内核内部数据.处理器状态一个或多个具有 ...

随机推荐

  1. [POI2011]Meteors

    嘟嘟嘟 做了几道题之后,对整体二分有点感觉了. 整体二分的本质就是二分答案.所以这道题二分的就是次数. 然后就是套路了,把小于\(mid\)的操作都添加减去,然后查询,如果查询的值\(x\)比给定值大 ...

  2. oracle 更新用户密码,授连接权限,

    1.授连接权限 grant connect to 用户名; ALTER USER 用户名 ACCOUNT UNLOCK; 2.更新密码 ALTER USER 用户名 IDENTIFIED BY 更新密 ...

  3. 使用iozone进行磁盘读写性能测试

    对于很多GIS工程师,经常需要对系统的磁盘性能进行测试,为了排查问题或者帮助用户进行系统设计. IOZone这个磁盘性能测试工具就是一个很方便的辅助工具. 下面就以个测试共享目录的读写性能为例,说明其 ...

  4. leetcode 217—Contains Duplicate

    Given an array of integers, find if the array contains any duplicates. Your function should return t ...

  5. jsp二(指令)

    一.jsp动作标签: 1)<jsp:forward> 请求转发 相当于之前的request.getRequestDispatcher(..).forward(..); <!--jsp ...

  6. CentOS中配置CDH版本的ZooKeeper

    三台CentOS:Host0,Host1,Host2 在三台中分别安装zookeeper-server yum install zookeeper-server -y 修改zookeeper的配置文件 ...

  7. HDU 3969 Hawk-and-Chicken(dfs+tarjan缩点优化,网上最详细解析!!!)

    Hawk-and-Chicken Time Limit: 6000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) ...

  8. Storm 运行例子

    1.建立Java工程 使用idea,添加lib库,拷贝storm中lib到工程中 2.拷贝wordcount代码 下载src包,解压找到 apache-storm-0.9.4-src\apache-s ...

  9. Java是如何读到hbase-site.xml 的内容的

    Java是如何读到hbase-site.xml 的内容的 Java客户端使用的配置信息是被映射在一个HBaseConfiguration 实例中. HBaseConfiguration有一个工厂方法, ...

  10. iscsi target tgt架构

    tgt是用户态实现的iscsi target,而iet(iscsi enterprise target)是在内核态实现的target,tgt相比于iet来说,因为其用户态实现,方便调试,新加入一些功能 ...