| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> linux内核设计和实现第三章节-----进程管理 -> 正文阅读 |
|
[系统运维]linux内核设计和实现第三章节-----进程管理 |
前言: 为什么要学习linux内核,随着ebpf的深入,对linux内核的熟悉程度,将会直接决定我们的kprobe的使用情况,尽管linux内核已经帮我们通过map定义了钩子 参考文献: <<linux内核设计和实现>> 参考网址: linux内核线程_huangweiqing80的博客-CSDN博客_内核线程 3.1 进程进程一般包括:可执行程序的代码,像打开的文件、挂起的信号,内核内部处理数据,处理器状态,一个或者多个具有内存映射的内存地址空间以及一个或者多个执行线程组成???????? 执行线程简称线程,每个线程都有一个独立的程序计数器、进程栈、进程寄存器。内核调度的对象是线程而不是进程,现在多线程程序司空见惯,对linux内核而言进程和线程其实并不是区别特别明显。 现在操作系统中有两种虚拟机制,包括虚拟内存和虚拟处理器,可能多个进程共享一个处理器,虚拟处理器会给这些进程一个虚假现象,让他们认为自己是单独使用一个cpu,线程之间一般共享虚拟内存,都有各自的虚拟处理器。 程序本身并不是进程,进程是出于执行期间的程序以及相关的资源总称。完全可能存在两个或者两个进程执行同一个程序。并且两个或者两个以上并存的程序还可以共享打开的文件,虚拟内存之类的资源。 在linux系统忠,操作系统通过调用fork去创建一个进程,调用fork的是父进程,产生的新进程是子进程,fork之后一般调用exec开辟新的地址空间,在现代linux内核中,fork实际上是由clone系统调用实现的。通过调用wait4系统调用查询子进程是否终结,父进程调用wait或者waitpid来回收子进程。 3.2 进程描述符及任务结构内核把进程的列表存放在叫做任务列表的双向链表中。链表中每一项类型都是task_struct、称为进程描述符(processer descriptor)的结构,该结构定义在<linux/sched.h> 文件中。进程描述符包含一个具体进程的所有信息。 进程描述符(process description)包括了打开的文件、进程的空间地址、挂起的信号以及进程的状态。 3.2.1 分配进程描述符linux通过slab分配器分配task_struct 结构,这样能达到复用对象和缓存的着色的目的。各个进程的task_struct存放在它的内核栈尾端。这样做是为了让x86寄存器只要通过栈栈指针就能找到他的位置。 在x86上,struct thread_info 在文件 <asm/thread_info.h>中定义如下:
?每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct 的指针。 3.2.2 进程描述符的存放内核通过一个唯一的进程标识值或pid来标识每个进程。pid默认的最大值是32768,尽管这个值也可以增加到400万。 如果确实需要修改的话我们可以通过修改/proc/sys/kernel/pid_max值来修改。
内核大部分处理程序是通过task_struct进行的。因此通过current这个宏去访问描述符的速度就至关重要,current再从thread_info的task域中提取并且返回task_struct的地址。
3.2.3进程状态TASK_RUNNING(运行) TASK_INTERRUPTIBLE(可中断) TASK_UNINTERRUPTIBLE(不可中断) __TASK_TRACED(被其他进程跟踪的进程) 3.2.4设置当前进程状态内核进程需要调整某个进程状态。这时最好使用set_task_state(task, state)函数
必要的时候设置内存屏障
3.2.5 进程上下文可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程地址空间执行。当一个程序执行系统调用就进入内核态了。我们称内核“代表进程执行”并处在进程上下文中,此时current宏依旧是有效的 3.2.6 进程家族树所有的进程都是pid为1的init进程的后代。每个task_struct 都包含一个只想其父进程task_struct、叫做parent指针,还包含一个叫children的子进程链表 访问父进程
访问子进程:
init 进程的进程描述符是 作为init_task 静态分配的。下面的代码可以很好的显示所有进程的关系:
任务队列本来是一个双向列表,对于给定的进程,获取链表中的下一个进程
获取上一个进程:
这两个分别通过 next_task(task)宏和prev_task(task)宏实现。 3.3 进程创建一般通过fork 去调用,fork后很多进程会执行exec去替换可执行环境。 fork创建的进程区别仅仅在于pid、ppid和某些资源的统计量。 3.3.1 写时拷贝这是我一个很久之前去面试过的问题了,早在unix是全量复制父进程内存,但是linux中做了改善,写时复制,只有在数据写入的时候,数据才会被复制。 3.3.2 fork()linux底层通过clone实现了fork。 fork vfork 和 __clone都通过传入不同的参数去调用clone,然后用clone去调用do_fork()。 do_fork 完成了创建的大部分工作,他通常定义在 kernel/fork.c 文件中。通过调用copy_process()函数,然后让进程开始运行。copy_process 做了什么呢? 1)通过调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程值相同。此时子进程和父进程的描述符是完全相同的。 2)检查并且确保新创建子进程后,当前用户所拥有的进程数目并没有超过给它分配的资源限制。 3)子进程着手与分进程区分开来。进程描述符内的许多成员都要被清0初始化值。那些不是继承而来的描述符成员,主要是信息。task_struct 许多成员依然未被修改 4)子进程的状态被设置为TASK_UNITERRUPTIBLE,以保证它不会投入运行。 5)copy_process 调用copy_flags 已更新task_struct 的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV 标志被清0.表明进程还没有调用exec函数的PF_FORKNOEXEDC标志被设置。 6) alloc_pid 为新进程分配一个pid 7)根据传递给clone参数标志,copy_process 拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。这些资源在进程间的所有线程之间共享;否则这些资源对每个进程是不同的,因此被拷贝到这里。 8)最后copy_process做扫尾工作并且返回一个指向子进程的指针。 fork 之后立马调用exec可以避免写时复制的开销。 3.3.3 vfork除了不拷贝父进程的页表项外,vfork 和 fork调用基本相同。 vfork 是通过在clone中加入一个标志位来调用实现的。 vfork 是如何运行的?在linux2.2以前是这么运行的: 1)在调用copy_process时候,task_struct 的vfor_done成员被设置位NULL。 2)执行do_fork后,如果给定特别标志,vfork_done会指向一个特殊地址 3)子进程线开始执行后,父进程不是马上恢复执行,而是一直等待,知道子进程调用vfork_done指针向父进程发送信号 4)在调用mm_release时候这个函数用于进程退出内存地址空间,并且检查vfork_done是否是空,如果不是空,向父进程发送信号 5)回到do_fork,父进程醒来并且返回。 这样子进程在新的空间地址运行,父进程也恢复在原空间地址运行,这样降低了开销,但不是最优性能。 3.4线程在linux中实现从linux内核角度看,内核看所有线程都当作进程来实现。内核并没有准备特别算法和数据结构,每个线程都有自己的task_struct ,所以它看起来像是一个普通进程。 3.4.1 创建线程fork的clone实现
vfork的clone实现:
在<linux/sched.h>中定义的clone标志位:
3.4.2内核线程内核线程和普通的线程区别在于没有地址空间,mm位NULL,内核线程只会在内核空间内运行,从来不会切换到用户空间。内核线程和普通线程一样,可以被调度,也可以被抢占。 pid1 是init 进程,pid 2 是内核线程?
kthread底层也是调用clone 产生的,具体api:
3.5 进程终结一般来说进程析构是自己引起的 比如说调用exit,当然也可能是外部,比如说,发送信号 大部分调用do_exit内核都会进行下面的操作: 1)task_struct 结构体的标志位设置位PF_EXTING 2)调用del_timer_sync 删除内核的定时器。 3)如果由进程会计的记账信息,具体看unix高级环境编程,会输出进程会计信息 4) 调用 exit_mm() 释放占用的mm_struct 5) 调用sem__exit 函数。如果进程排队等待IPC信号,则离开队列。 6) 调用exit_files() 和 exit_fs(),分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用技术的值降为0,那么久嗲表没有进程使用资源,则可以释放。 7) 接着把存放在task_struct 的exit 成员中的任务退出代码设置为exit提供的退出代码,或者去完成任何由其他内核机制规定的退出动作。退出代码放在这里供父进程检索使用。 8)调用exit_notify 向 父进程发送信号,给子进程重新找养父,养父位线程组中的其他线程或者位init进程,并把进程状态设置位EXIT_ZOMBILE 9) do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBILE状态的进程不会再次调度,这是进程执行的最后一段代码,do_exit 永远不返回。 到了这里进程变成了EXIT_ZOMBILE状态,但是进程内部的 task_struct 、thread_info等架构提荣然不会被释放,只有在父进程检索到信息后,通知内核是无关信息,由进程持有者去释放,归还资源给内核。 3.5.1 释放进程描述符尽管调用do_exit()后,线程已经进入僵死状态,但是进程描述符task_struct 依然没有被释放,只有在父进程收到SIGCHILD通知后调用wait一族的函数才会被真正释放,退出步骤依次是: 1)调用__exit_signal(),该函数调用_unhash_process(),后者调用detach_pid()从pidhash上删除这个线程,同时也从任务列表中删除这个进程。 2)__exit_signal() 释放目前已经将死进程所使用的所有资源,并进行最后的统计和记录 3)如果这个进程是线程组最后一个进程,并且领头进程已经死了,那么release_task()就要通知将死进程的领头进程的父进程。 4)release_task() 调用put_task_struct() 释放进程内核栈和thread_info结构所占领的页,并且释放task_struct 所占领的slab高速缓冲区 至此进程描述符的资源被全部释放掉。 3.5.2 孤儿进程的进退维谷如果父进程在子进程之前退出,必须有一个进制确保子进程能找到一个父进程,否则孤儿进程会永远僵死,白白占用内存。在do_Exit()中会调用exit_notify(),这个函数会调用forget_original_parent(),后者会调用find_new_reaper()来执行寻父过程 find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)
|
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/15 23:44:55- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |