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

2022/1/15 7:10:40

本文主要是介绍《Linux内核设计与实现》之进程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 1 进程
    • 1.1 两个虚拟化
    • 1.2 任务队列
    • 1.3 task_struct
    • 1.4 进程家族树
    • 1.5 进程创建
      • 1.5.1 fork() 函数
    • 1.6 进程终结
      • 1.6.1 do_exit()
      • 1.6.2 删除进程描述符
  • 2 线程
    • 2.1 线程的创建
    • 2.2 内核线程

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,只在内核空间工作,不会切换到用户空间。



这篇关于《Linux内核设计与实现》之进程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程