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

进程管理

By20135203齐岳

进程

  • 进程:处于执行期的程序。包括代码段和打开的文件、挂起的信号、内核内部数据、处理器状态一个或多个具有内存映射的内存地址空间或执行线程等其他资源。

  • 线程:是在进程中活动的对象。每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而非进程。操作系统中进程提供两种虚拟机制:虚拟存储器和虚拟内存。

  • 程序:本身并不是进程,进程是处于执行期的程序以及相关的资源的总称。不同的进程可以执行同一个程序。

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

进程描述符及任务结构

内核把进程的列表存放在叫做任务队列的双向循环链表中。链表中的每一项都是进程描述符。类型为task_struct,里面包含的数据有:它打开的文件、进程的地址空间、挂起的信号、进程的状态以及其他相关信息。

分配进程描述符

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

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

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

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

进程描述符的存放

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

pid类型为pid_t,实际上就是一个int类型。内核把每个进程的pid存放在各自的进程描述符中。

pid最大值默认设置为32768,通过修改/proc/sys/kernel/pid_max可以提高上限。

获得指向task_struct指针的方法:

通过current宏查找到当前正在运行进程的进程描述符。
在内核栈的尾端创建thread_info结构
current通过current_thread_info()把栈指针的后13个有效位屏蔽用来计算出thread_info的偏移。
从thread_info的task域中提取并返回task_struct的地址。

进程状态

进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然属于下列的五种状态之一:

  • TASK_RUNNING(运行):进程是可执行的,或者正在执行,或者在运行队列中等待执行。是进程在用户空间中执行的唯一可能的状态。

  • TASK_INTERRUPTIBLE(可中断):进程正在睡眠/被阻塞。处于此状态的进程会因为收到信号而被唤醒从而准备投入运行。

  • TASK_UNINTERRUPTIBLE(不可中断):睡眠/被阻塞进程不被信号唤醒。这个状态通常在进程必须等待时不受干扰或等待时间很快就会发生时出现。

  • TASK_TRACED:被其他进程跟踪的进程。

  • TASK_STOPPED(停止):进程停止执行;进程没有投入运行也不能投入运行。接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号时,或者调试时收到任何信号,都可以进入这种状态。

设置当前进程状态

内核需要使用set_task_state(task,state)函数来调整某个进程的状态。

set_task_state(task,state); //将任务task的状态设置为state
一般情况下等价于task->state=state; set_current_state(state)//等价于set_task_state(current,state)

进程上下文

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

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

进程家族树

所有的进程都是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静态分配的。所有进程之间的关系:

struct task_struct *task;
for(task=current;task1=&init_task;task=task->parent)
;
/*task现在指向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)宏,依次访问整个任务队列,每次访问任务指针都指向链表中的下一个元素。

struct task_struct *task;

for_each_process(task){
/* 它打印出每一个任务的名称和pid */
printk("%s[%d]\n",task->comm, task->pid);
}

进程创建

Unix的进程创建机制:

  • fork():通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅在于PID,PPID和某些资源和统计量

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

写时拷贝

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

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

fork的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符,进程创建后都会马上运行一个可执行的文件,避免拷贝大量不会被使用的数据,从而实现优化。

fork()

Linux通过clone()系统调用实现fork()。实现过程如下:

  • fork()、vfork()、 _ clone()都根据各需要的参数标志调用clone()。由clone()去调用do _ fork()。

  • do _ fork()调用copy _ process()函数,然后让进程开始运行。

  • 返回do _ fork()函数,如果copy _ process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

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

vfork()

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

子进程作为父进程的一个单独的线程在它的地址空间里运行 ,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。

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

  • 调用copy _ process()是,task _ struct的vfor _ done成员被设置为NULL。

  • 执行do _ fork()时,如果给定特定标志,则vfor _ done会指向一个特定地址。

  • 子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,知道子进程通过vfor _ done指针向它发送信号。

    在调用mm _ release()时,该函数用于进程退出内存地址空间,并且检查vfor _ done是否为空,如果不为空,则会向父进程发送信号。

  • 回到do_fork(),父进程醒来并返回。

线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象概念,该机制提供了在同一程序内共享内存地址空间运行的一组线程,可以共享打开的文件和其他资源,支持并发程序设计,在多处理器系统上可以保证真正的并行处理。

Linux内核的角度来看并没有线程这个概念,它把所有线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。

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

创建线程

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

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

普通的fork:

clone(SIGCHLD, 0);

vfork():

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

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

内核线程

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

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

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

struct task_struct *kthread_creat(int (*threadfn)(void *data),
void *data,const char namefmt[],...)

kthread _ create()通过clone()系统调用创建内核线程后,处于不可运行状态,如果不通过wake _ up _ process()明确唤醒它,不会主动运行。创建一个进程并让它运行起来 ,可以调用kthread _ run()。

struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,const char namefmt[],...)

内核线程启动后就一直运行直到调用do _ exit()退出,或者内核的其他部分调用kthread _ stop()退出,传递给kthread _ stop()的参数为kthread _ create()函数返回的task _ struct结构的地址。

int kthread_stop(struct task_struct *k)

进程终结

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

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

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

大部分依赖于do_exit()来完成。

删除进程描述符

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

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

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

孤儿进程

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

解决方法有:

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

具体实现的过程:

  • do _ exit()调用exit _ notify()
  • exit _ notify()会调用forget _ original _ parent()
  • forget _ original _ parent()会调用find _ new _ reaper()
  • 遍历所有子进程并为它们设置新的父进程。
  • 调用ptrace _ exit _ finish()同样进行新的寻父过程,是给ptraced的子进程寻找父亲。
  • init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳的更多相关文章

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

    <Linux内核设计与实现>课本第四章自学笔记 进程调度 By20135203齐岳 4.1 多任务 多任务操作系统就是能同时并发的交互执行多个进程的操作系统.多任务操作系统使多个进程处于堵 ...

  2. 《Linux内核设计与实现》课本第五章学习笔记——20135203齐岳

    <Linux内核设计与实现>课本第五章学习笔记 By20135203齐岳 与内核通信 用户空间进程和硬件设备之间通过系统调用来交互,其主要作用有三个. 为用户空间提供了硬件的抽象接口. 保 ...

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

    <Linux内核设计与实现>课本第十八章自学笔记 By20135203齐岳 通过打印来调试 printk()是内核提供的格式化打印函数,除了和C库提供的printf()函数功能相同外还有一 ...

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

    第三章  进程管理 姓名:王玮怡  学号:20135116 一.进程 1.进程的含义 进程是处于执行期的程序以及相关资源的总称,程序本身并不是进程,实际上就是正在执行的代码的实时结果.Linux内核通 ...

  5. 《Linux内核设计与实现》第三章读书笔记

    一.进程(任务)描述 1.进程是处于执行期的程序:除了可执行程序代码,还包括打开的文件.挂起的信号.内核内部数据.一个或者多个执行线程等多种资源 线程是在进程活动中的对象:内核调度的对象是线程而不是进 ...

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

    一.进程 1.进程就是处于执行期的程序(目标码存放在某种存储介质上).但进程并不仅仅局限于一段可执行程序代码,通常进程还要包含其他资源.执行线程,简称线程(thread),是在进程中活动的对象. 2. ...

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

    <Linux内核设计与实现> 第一二章学习笔记 第一章 Linux内核简介 1.1 Unix的历史 Unix的特点 Unix很简洁,所提供的系统调用都有很明确的设计目的. Unix中一切皆 ...

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

    <Linux内核设计与实现>第四章学习笔记           ——进程调度 姓名:王玮怡  学号:20135116 一.多任务 1.多任务操作系统的含义 多任务操作系统就是能同时并发地交 ...

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

    <Linux内核设计与实现>第五章学习笔记 姓名:王玮怡  学号:20135116 一.与内核通信     在Linux中,系统调用是用户空间访问内核的唯一手段:除异常和陷入外,它们是内核 ...

随机推荐

  1. sql 查询表中所有字段的名称

    最近工作用到SQL语句查询表中所有字段的名称,网上查询,发现不同数据库的查询方法不同,例如: SQL server 查询表的所有字段名称:Select name from syscolumns Whe ...

  2. java udp与tcp

    一:基础  NET基本对象java.net.InetAddress类的使用 IP地址是IP使用的32位(IPv4)或者128位(IPv6)位无符号数字,它是传输层协议TCP,UDP的基础.InetAd ...

  3. 初识NodeJS,一个基于GoogleV8引擎的Javascript运行环境

    思考 首先我们来思考一个问题:我们都知道几乎所有现代主流浏览器都全面支持了ECMAScript 5.1版标准,而JavaScript的标准是ECMAScript.那么我们就容易认为JavaScript ...

  4. django之分页、cookie装饰器

    一.分页代码如下 from django.utils.safestring import mark_safe class Page: def __init__(self, current_page, ...

  5. 2016年12月29日 星期四 --出埃及记 Exodus 21:24

    2016年12月29日 星期四 --出埃及记 Exodus 21:24 eye for eye, tooth for tooth, hand for hand, foot for foot,以眼还眼, ...

  6. 简单的方式优化mysql

    参考博客 自己笔记本上向mysql导入txt数据,有一个table导入了将近4个小时,而且多个table之间都是相互之间存在关系的,所以做联合查询的时候你会发现问题会十分的多,我之前联合查询两个表就死 ...

  7. python之初级学习

    一.python安装 1.下载安装包(本人使用python3.5.1) https://www.python.org/downloads/ 2.安装python-3.5.1.exe 本人下载的是pyt ...

  8. Xcode及obj-c的基础知识

    1, 从简单的例程来看基本语法: 下面的代码是通过OSX-Application-Command Line Tool生成的: #import <Foundation/Foundation.h&g ...

  9. javascript面向对象(学习和理解)

    js中创建变量基本如下: var name = 'saodiseng'; var email = 'wuyucoder@126.com'; var website = 'http://www.cnbl ...

  10. Rails中的缓存

    最近学习Rails. 看到如下代码: <% if notice %> <p id="notice"><%= notice %></p> ...