ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

《Linux内核设计与实现》之进程

2022-01-14 16:31:59  阅读:148  来源: 互联网

标签:task struct exit 内核 Linux 进程 线程


文章目录

1 进程

  • 进程=程序+资源,资源包括打开的文件、内核内部数据、CPU状态、挂起的信号
  • Linux不特意区分线程和进程(二者都由task_struct结构体表示),线程是一种特殊的进程

1.1 两个虚拟化

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

  • 虚拟内存:实际上是使用硬盘补足进程的内存,一般电脑的内存为8G,但实际上一个进程可能就要用到多于8G的内存。还有,多个线程共享进程的虚拟内存。
  • 虚拟处理器:给进程一种自己独占CPU的假象,但实际上是多个进程共用CPU。还有,每个线程都有自己的虚拟处理器(内核调度的是线程而非进程)。

1.2 任务队列

内核把进程的列表放到名为任务队列的双向循环链表中。链表中的节点类型为task_struct(进程描述符),包含了内核管理进程的所有信息。

Linux通过Slab分配器分配task_struct结构体,在内核栈尾部创建thread_info结构体。
通过预先分配和使用task_struct,避免动态分配和释放的资源消耗。
在这里插入图片描述
在这里插入图片描述

1.3 task_struct

task_struct状态,僵尸、孤儿进程

1.4 进程家族树

父进程:task_struct中包含名为parent、类型为task_struct的父进程
子进程:还有名为children的子进程链表
兄弟进程:父进程相同的进程被称为兄弟进程
总结:根据这个树形结构,可以从一个进程找到任意一个其他的进程

系统启动的最后阶段,会启动PID=1init进程,init进程会读取系统的初始化脚本完成系统启动。

1.5 进程创建

Unix采用两个函数来创建进程:fork() 和exec()

1.5.1 fork() 函数

fork() 通过拷贝当前进程创建子进程,子进程与父进程的不同在于:PID、PPID(父进程ID)和某些资源和统计量(例如挂起的信号)

Linux的fork() 使用写时拷贝(copy-on-write)页实现。

  1. 内核此时并不复制整个进程空间,而是让父进程和子进程共享同一份拷贝;
  2. 需要写入时,进程才会复制数据,此前,进程只以只读权限共享数据;

fork() 的实际开销就是复制父进程的页表和创建子进程的task_struct(进程描述符),优点明显,避免了拷贝大量根本不需要的数据,加快了执行速度。

1.6 进程终结

进程结束时要释放资源并告知父进程,进程的结束大概率是因为显式/隐式调用了exit() 系统调用

1.6.1 do_exit()

exit() 函数可以显示调用,main() 最后也会隐式调用exit() ;但是,进程接收到无法处理也无法忽略的信号时,也可能被动退出。无论是主动还是被动,大部分都会调用do_exit() 函数执行进程终结,该函数步骤:

  1. 将task_struct(进程描述符)的标志成员设置为PF_EXITING;

  2. 调用del_timer_sync() 删除任意内核定时器。根据返回结果,他确保没有定时器在排队,也没有定时任务在运行;

  3. 如果BSD的进程记账功能是开启的,do_exit() 调用acct_update_integrals() 来输出记账信息;(进程记账好像是计算进程占用CPU时间之类的)

  4. 调用exit_mm() 释放进程占用的mm_struct,若没有别的进程使用(即没有被共享)就彻底释放;

  5. 调用em_exit() 。如果进程排队等待IPC信息(进程间通信),则让它离开该队列;

  6. 调用exit_files() 和exit_fs() ,代表文件描述符、文件系统数据的引用计数,如果降为0,则代表没有进程使用该资源,这时进程才能被释放;

  7. 把task_struct中的exit_code成员(退出代码)置为exit() 函数或其他。供父进程检索。

  8. 调用exit_notify() 向父进程发送信号,给该进程找养父,养父为线程组的其他线程或init进程,并把进程状态改为EXIT_ZOMBIE;(这段不懂的可以看看

  9. 调用schedule() 切换到新的进程。处于EXIT_ZOMBIE的进程(僵尸进程)不会被调度,这时进程的最后代码。do_exit() 永不返回。

    至此,与该进程关联的所有资源(只被该进程使用)被释放。 进程无法被使用(也没有地址空间供它使用)且处于EXIT_ZOMBIE。这时该进程就是僵尸进程,唯一占用的资源就是内核栈、thread_info结构和task_struct结构。 第八点说了会向父进程发送信息,然后父进程理睬的话就会释放该进程所有资源

1.6.2 删除进程描述符

do_exit()执行完后,该进程状态为EXIT_ZOMBIE(僵尸进程),同时父进程收到子进程结束信号,处理完后才会释放子进程的task_struct。

父进程接受信号的操作是调用wait()函数:该函数会挂起当前进程,直到有子进程退出,返回退出子进程的PID。接下来,调用release_task()函数执行删除进程描述符:

  1. 调用_exit_signal(),该函数调用_unhash_process() ,后者又调用detach_pid() pidhash删除该进程,同时也要从任务列表中删除该进程

  2. _exit_signal() 释放僵尸进程所使用的的所有剩余资源,并进行最终统计和记录;

  3. 如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task() 就要通知僵死的领头进程的父进程;

  4. 调用put_task_struct() 释放进程内核栈和thread_info结构所占有的页,并释放task_struct所占的slab高速缓存;

    至此,进程描述符和进程所占有的资源都释放了。

2 线程

Linux内核其实并不区分进程或线程,因为每个线程也是用task_struct表示,内核只把一组线程当做共享某些资源的普通进程,简单高效。而例如微软的操作系统有专门的线程机制。

2.1 线程的创建

虽然内核不区分,但接下来的讲解为方便理解,可以把进程当作一个资源的容器,线程才实际上执行任务。

线程的创建与进程类似,都是调用clone() 函数,只不过需要一些参数指定要共享的资源:
clone(CLONE_VM | CLONE_FS, 0),创建的进程和这个父进程就是所说的线程。参数有以下:

clone函数的参数
在这里插入图片描述

2.2 内核线程

内核线程与普通线程的不同点在于:指向地址空间的mm指针为NULL,只在内核空间工作,不会切换到用户空间。

标签:task,struct,exit,内核,Linux,进程,线程
来源: https://blog.csdn.net/cyx1812431306/article/details/122396529

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有