惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
About on SuperTechFans
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
G
Google Developers Blog
J
Java Code Geeks
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
H
Heimdal Security Blog
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
S
Security @ Cisco Blogs
Latest news
Latest news
I
Intezer
L
Lohrmann on Cybersecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
月光博客
月光博客
T
Threatpost
博客园 - 【当耐特】
S
Schneier on Security
P
Privacy International News Feed
G
GRAHAM CLULEY
T
Tenable Blog
AWS News Blog
AWS News Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
博客园 - Franky
Engineering at Meta
Engineering at Meta
美团技术团队
S
Secure Thoughts
T
Troy Hunt's Blog
Microsoft Security Blog
Microsoft Security Blog
SecWiki News
SecWiki News
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More

个人博客

leetcode经典动态规划解题报告 Netty启动原理 web安全基础知识一 SpringBoot自动配置原理 跟踪SpringMVC请求过程 Netty之ChannelHandler 负载均衡的实现方式与算法 Netty之ByteBuf Netty线程模型 段页式内存管理 fork()与写时复制 fork()与写时复制 Dubbo小知识点总结 MyBatis的使用回顾 Dubbo集群容错 Dubbo服务调用过程 InnoDB的锁 SpringBoot启动原理 Maven和Git命令的一些总结 高可用Redis:Redis Cluster
Linux内核设计与实现读书笔记一
zofun · 2020-05-15 · via 个人博客

进程管理

进程

相关概念

进程就是处于执行期的程序。进程是处于执行期的程序以及相关资源的总称。
线程是进程中的活动对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。
内核调度的对象是线程而不是进程。

进程描述符及任务结构

内核吧进程的列表存放在叫做任务队列的双向循环列表中。链表中每一项的类型都是task_struct、被称为进程描述符。该结构包含了内核管理一个进程所需的所有信息。

分配进程描述符

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

在2.6之前为了减少对寄存器的使用,task_struct存储在内核栈的尾端,而在2.6之后使用slab分配器动态生成task_stuct,所以只需要在栈第或栈顶创建一个thread_info,它的内部包含了task_struct等信息。

YwEt4s.png

每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。

进程描述符的存放

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

在内核中,任务访问通常需要获得指向其task_struct的指针。内核使用current宏可以计算出当前进程task_struct的指针。

进程管理

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

  • TASK_RUNNING(运行):进程是可执行的,它或者正在执行,或者在运行队列中等待执行。
  • TASK_INTERRUPTIBLE(可中断):进程正在随眠,等待某些条件达成。一旦这些条件达成,内核就会把进程状态设置为运行。
  • TASK_UNINTERRUPTIBLE(不可中断):除了就算接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。
  • _TASK_TRACED:被其它进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。
  • _TASK_STOOPED:进程停止运行。

Ywut5F.png

进程上下文

一般的程序是在用户空间执行,当程序进行了系统调用或触发了某一异常,那么它就会陷入到内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下问中current宏是有效的。

进程创建

在Unix操作系统中进程的创建分为两步完成,第一步是调用fork()方法拷贝当前进程创建一个子进程(父子进程的唯一区别就是PID),然后调用exec()方法载入可执行文件并开始执行。

而Linux对整个进程的创建做出了优化:

写时拷贝

写时拷贝时一种可以推迟甚至免除拷贝数据的技术,在创建进程的时候,内核并不需要再一开始就复制整个进程地址空间,而是让父子进程共享一个拷贝。只有再需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是所,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享,这种技术使得地址空间上的页的拷贝被推迟到实际发生写入的时候才进行,在页根本不会被写入的情况下(比如fork后立即调用exec)它们就无需复制了。

也就是只有进程空间的各段的内容要发生变化时,才将父进程的内容复制一份给子进程。

fork()

Linux通过clone()系统调用来实现fork()。然后clone()去调用do_fork()do_fork()完成了创建中的大部分工作,该函数调用了copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作如下:

  1. 调用dup_task_struct()为新建成创建一个内核栈,thread_info结构和task_struct,这些只与当前进程的只相同。此时,子进程和父进程的描述符使完全相同的。
  2. 检测创建了这个子进程之后,当前用户所拥有的进程数目有没有超出给它分配资源的限制。
  3. 子进程着手使自己与父进程区别开来。将进程描述符内的许多成员清零或设置为初始值。
  4. 将子进程的状态设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。
  5. copy_process()调用copy_flags()以更新task_struct的flag成员。
  6. 调用alloc_pid()为新进程分配一个有效的PID。
  7. 根据传递给clone()的参数标志,决定拷贝或共享打开的资源。
  8. 最后copy_process做扫尾工作并返回一个指向子进程的指针。

vfork()

除了不拷贝父进程的页表项外,vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的线程在他的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec().

vfork()系统调用的实现是通过项clone()系统调用实现的:

  1. 在调用copy_process()时,task_structvfor_done成员被设置为NULL。
  2. 在执行do_fork()时,如果给定特别标志,则vfork_done会指向特定地址。
  3. 子进程先开始指向后,父进程不是马上恢复执行,而是一直等待,直到父进程通过vfork_done指针向它发送信号。
  4. 在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfork_done是否为空,如果不为空,则会向父进程发送信号。
  5. 回到do_fork(),父进程醒来并返回。

线程在Linux中的实现

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

在Linux中的内核实现中,它不严格区分线程和进程,线程仅仅被视为一个与其它进程共享某些资源的进程。

创建线程

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

内核线程

内核进程需要在后台执行一些操作,这些操作由内核线程来完成。

内核线程和普通的进程的区别在于内核线程没有独立的地址空间。它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。内核线程也只能由其它内核线程创建。

进程终结

当一个进程终结时,内核必须要释放它所占有的资源并通知其父进程。

进程终结时,最终大多都是通过do_exit()来完成的。它主要完成了以下工作:

  1. task_struct中的标志成员设置为PF_EXITING
  2. 调用del_timer_sync()删除任一内核定时器,根据返回的结果,保证没有定时器在排队,也没有定时器处理程序在运行。
  3. 如果BSD的进程计帐功能时开启的,那么会调用acct_update_integerals()来输出记账信息。
  4. 调用exit_mm()函数释放进程占用mm_struct,如果没有别的进程使用它,就释放。
  5. 接下来调用sem_exit()函数,如果进程排队等候IPC信号,它则离开队列。
  6. 调用exit_files()exit_fs(),以分别递减文件描述符,文件系统数据的引用计数。如果引用计数降为0,那么它代表没有进程使用相应的资源,此时就可以释放。
  7. 接着把存放在task_structexit_code成员中的任务退出代码置为由exit()提供的退出代码。
  8. 调用exit_notify()向父进程发送行信号,给子进程重新找养父,养父为线程组中的其它进程或者init进程,并把进程状态设置为EXIT_ZOMBIE.
  9. do_exit()调用schedule()切换到新的进程,因为处于EXIT_ZOMBIE状态的进程不会再调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。

删除进程描述符

调用do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还是保留了它们的进程描述符。这样做的好处就是有办法再进程终结后仍然能够获得它的信息。

当需要释放进程描述符的时候,会调用release_task().

处理孤儿进程

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

处理孤儿进程的方案是,首先会尝试在当前线程组内找一个线程作为父进程,如果不行,就让init做它们的父进程。

进程调度

多任务

多任务操作系统就是能够同时并发地交互执行多个进程地操作系统。

多任务系统可以划分为两类:非抢占式多任务和抢占式多任务。Linux提供了抢占式多任务模式。

在此模式下,由调度程序来决定什么时候停止一个进程的运行,以便其它进程能够得到执行的机会。这个强制挂起的动作叫做抢占。进程在被抢占之前能够运行的时间式预先分配号的,叫做进程的时间篇。

在非抢占式多任务模式下,除非进程自己主动停止运行,否则它会一直执行。进程主动挂起自己的操作称为让步。

Linux的进程调度

Linux2.5之后采用了一种叫做O(1)的调度程序。

调度策略

IO消耗型和处理器消耗型的进程

进程可以分为IO消耗型和处理器消耗性。前者指进程的大部分时间用来提交IO请求或是等待IO请求。因此,这样的进程进程处于可运行的状态,但通常都是运行短短的一会,因为它在等待更多的IO请求时最后总会阻塞。

而处理器消耗型进程把时间大多用在执行代码上,除非被抢占,否则它门通常都一直不停的运行,因为它们没有太多的IO需求。

调递策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐率)。

进程优先级

调度算法中最基本的一类就是基于优先级的调度。这是一种根据进程的价值和对处理器时间的需求来对进程分级的想法。

Linux采用了两种不同的优先级范围,第一种是用nice值,它的范围是从-20到+19,nice值越小,优先级越高。第二种范围是实时范围,其值是可配置的,默认情况下它的变化范围是从0到99,值越大,优先级越大。

实时优先级和nice优先级处于互不相交的两个范畴。

时间片

时间片是一个数值,它表示进程在被抢占前所能持续运行的时间。调度策略必须规定一个默认的时间片。

一个处于就绪状态的进程能够进入运行态的完全是由进程优先级和是否有时间片决定的。

Linux调度算法

Linux中提供了一种CFS调度算法,即完全公平调度算法。CFS的实现思路就是根据进程的权重分配运行时间。这里的权重其实是和进程的nice值之间有一一对应的关系,可以通过全局数组prio_to_weight来转换。
$$
分配给进程的运行时间=调度周期*进程权重/所有进程权重之和
$$

CFS调度算法实现

调度器实体结构

CFS不再有时间片的概念,但是它也必须维护每个进程运行的时间记账,因为它需要确保每个进程只在分配给它的时间片内运行。CFS使用调度器实体结构来最终进程运行记账。

YDtIfA.png

虚拟实时

vruntime变量存放进程的虚拟运行时间,该运行时间的计算是经过了所有可运行进程总数的标准化。

进程选择

当CFS需要选择下一个运行进程是,它会挑一个具有最小vruntime的进程。CFS使用红黑树来组织可运行进程队列,并利用其迅速找到vruntime值最小的进程。

休眠和唤醒

等待队列

休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单链表。

唤醒

唤醒操作通过函数wake_up()进行,它会唤醒指定的等待队列上的所有进程。它调用函数wake_up()进行,它会唤醒指定的等待队列上的所有进程。

抢占

用户抢占

用户抢占在以下情况下产生:

  • 从系统调用返回用户空间时
  • 从中断处理程序返回用户空间时

内核抢占

内核抢占会发生在:

  • 中断处理程序正在执行,且返回内核空间之前。
  • 内核代码再一次具有可抢占性的时候。
  • 如果内核中的任务显示地调用schedule()
  • 如果内核中的任务阻塞。