LKD读书笔记:红黑树在内核中的实现

LKD(Linux内核设计与实现) Chapter 6 Kernel Data Structures 继续谈数据结构,本篇来聊聊红黑树。红黑树是一种基本平衡的二叉树,在提供良好的搜索性能的同时,插入操作也不会引入过大的再平衡开销。 对红黑树的简单描述 红黑树是这样一种特殊的二叉树: 每个节点带颜色,非红即黑 叶子节点必须存在并且是黑色的 如果一个节点是红色的,那么它的两个子节点必须是黑色的 从一个节点到其下辖的叶子结点之间如果存在多条路径,那么每条路径上的黑色节点个数必须等同 以上所有的规则就是为了保证一条特性: 从根节点开始的最远路径的长度不会超过最短路径的两倍。 这条特性限制了红黑树最差情况,保证了搜索性能。 Linux的红黑树实现 Linux的数据结构实现基本有一个共同点,我把它叫做宿主内嵌,看下面的例子: struct rb_node node作为红黑树的数据结构,是内嵌到宿主数据结构struct mytype中去的。这样做的明显好处是集约化,不必再专门为struct rb_node node单独分配内存,然后数据的读取更具有本地性,会带来更好的cache性能。 第二个问题,因为C语言的限制,Linux的红黑树实现并没有提供一套很完整的操作,类似查找和插入的操作需要使用者自行实现。但是Linux提供了工具箱,可以帮你很容易实现各种操作。让我们来看看内核文档中的一个例子: 红黑树的实现在lib/rbtree.c,使用前需要包含头文件#include <linux/rbtree.h>。   

March 24, 2016

LKD读书笔记:内核数据结构初窥,链表、队列和映射

LKD(Linux内核设计与实现) Chapter 6 Kernel Data Structures 这个章节主要阐述了内核中主要的几种数据结构,如链表、队列以及映射。Linux内核是用C语言写的,所以这些数据结构的实现受限于C语言的特点,大都没有精致的封装,很多细节需要写代码的人自己敲定。学习这些数据倒是可以帮助大家更好理解C语言一些精巧高效的使用方式。 链表 链表应该是内核最通用的结构了。一般来说,在C++这种支持泛型的语言中,链表作为是一个单独的数据类型使用,要存储的数据是被存放到链表中去的;然而Linux链表的实现方式比较奇特,这也是由于C不支持泛型,它把链表作为属性插入其他数据结构中去,这个做法英文叫做Embedded Anchor,下面是一个例子: 在上面的例子中,往struct dog嵌入一个属性struct list_head list,这样struct dog就可以通过这个属性变成一个双向链表了。struct list_head list的定义在include/linux/types.h: 可以通过INIT_LIST_HEAD宏来初始化链表: include/linux/kernel.h提供了一个宏container_of(ptr, type, member)可以从链表节点找到其所在结构体的指针: include/linux/list.h定义了很多链表的操作,其中有很多宏用来帮助遍历链表, 其中有代表性的如: list_for_each(pos, head) list_for_each_entry(pos, head, member) 上面只是简单介绍了一下Linux的链表的特点。由于是用C语言实现的,所以Linux采用了另辟蹊径的方式,将链表节点嵌入于不同的数据结构体中,来实现链表的泛型。这种可行性也基于一个事实,Linux的链表只为内核中的数据结构所用,链表和链表的使用者在同一个维度,链表假设它的使用者足够聪明能把问题搞对。 以此类推,Linux内核数据结构的封装程度都不高,他们不需要像C++STL库那样封装得密不透风。因为STL库不知道谁会使用它,以及以怎么样的方式使用它,所以它要把自己的责任划清。而内核的数据结构的使用者知道自己在干什么,过度的封装反而是枷锁。 队列和映射 内核的队列叫做kfifo(include/linux/kfifo.h),是一个定长的循环队列,需要在初始化的时候分配或者指定缓冲区: kfifo_alloc(fifo, size, gfp_mask) 初始化一个fifo并为其分配缓冲区 kfifo_init(fifo, buffer, size) 使用指定的缓冲区来初始化一个fifo 然后是一些常规的操作: kfifo_in(fifo, buf, n) 将数据放到fifo kfifo_out(fifo, buf, n) 从fifo取出数据 kfifo_out_peek(fifo, buf, n) 从fifo窥取数据 映射是一种关联数据类型,把一个键关联到一个值。因为内核有好多操作都是代表进程执行的,所以这里的映射特指以用户进程id为键的映射,这个数据类型的名字叫idr(include/linux/idr.h)。映射的一般操作包括创建、插入、删除,和查找: void idr_init(struct idr *idp); 初始化这个idr int ida_pre_get(struct ida *ida, gfp_t gfp_mask); 获取一个id前预先分配空间 int ida_get_new_above(struct ida *ida, int starting_id, int *p_id); 获取一个最小的空闲id void idr_remove(struct ida *ida, int id); 删除一个id void idr_destroy(struct ida *ida); 销毁一个idr    

March 9, 2016

LKD读书笔记:vsyscall和vdso

LKD(Linux内核设计与实现) Chapter 5 System Calls以软中断或指令方式执行的系统调用,需要切换到内核空间,是一个比较慢的操作。例如像gettimeofday()这种,若每次为了从内核读取时间值而都切换上下文的话,成本就太高了。所以Linux推出了一些措施,来帮助这些系统调用执行更快,那就是vsyscall和vdso。在64位系统上执行cat /proc/self/maps可以看到: vsyscall 首先谈vsyscall,这是内核最先引入的机制。内核直接把类似gettimeofday()这种功能简单的函数的二进制实现,直接映射到任务的用户空间中,这样任务就可以在用户空间把这些系统调用当成普通函数来用,这样效率就大大提升。 但vsyscall有一些致命的问题,一是vsyscall的用户空间映射所在的地址是固定不变的,容易被别有用心的人利用;二是vsyscall能支持的系统调用数有限,不利于扩展。所以,渐渐地一些替代方案就产生了。 vdso vdso是vsyscall的主要替代方案,全称是Virtual Dynamic Shared Object. 这是一个虚拟动态链接库,内核把那些系统调用放到这里面,然后用户程序在启动的时候通过动态链接操作,把这个vdso链到自己的内存空间中来。动态链接保证了vdso每次所在的地址都不一样,所以不太容易被利用,而且可以支持数量较多的系统调用。 Glibc封装 Glibc为系统调用提供了封装,保证使用的是当前机器上能够支持的最快的调用。我们所要做的只是直接调用Glibc的接口函数而已,如gettimeofday。但是Linux也提供了syscall()来让你直接通过中断的方式进行系统调用,下面是一个例子:   文章中涉及的任何代码都参考Linux v4.4。 What are vdso and vsyscall?{.question-hyperlink} On vsyscalls and the vDSO “Creating a vDSO: the Colonel’s Other Chicken” What are vdso and vsyscall?{.question-hyperlink} Linux syscall, vsyscall, and vDSO… Oh My! What is linux-gate.so.1? syscall(2)   

March 7, 2016

LKD读书笔记:进入系统调用的方式

LKD(Linux内核设计与实现) Chapter 5 System Calls当一个进程要进行系统调用的时候,它要先停止,然后把执行交给内核,这个过程英文叫做trapping。trap在英文中是陷阱的意思,用在系统调用上算是非常贴切。 目前在x86系统上,进入系统调用的方式有两种,一是通过软中断,二是通过特殊指令。不管何种方式,每个系统调用都由一个向量标识,可带若干参数。 软中断方式进入 这种方式下,程序在用户空间由0x80中断进入系统调用,0x80的中断处理函数根据向量标识来识别系统调用。在使用i386ABI的系统,向量标志在arch/x86/entry/syscalls/syscall_32.tbl定义: write()的向量标识是4,exit()的是1,下面的hi.s举例看如何在汇编代码中调用这两个系统调用: 使用gcc命令gcc hi.s -o hi编译后执行./hi,可见输出: sysenter/syscall指令进入 采用软中断的方式存在一个问题,就是操作比较繁琐,要先找到系统调用表,然后查表再跳转到执行代码段。如果能把系统调用表地址和执行代码段地址硬编码到指令中,就可以省略一些内存访问操作,加快速度。这些操作由相应的的sysenter/syscall指令实现,sysenter一般应用在32位系统,syscall一般应用在64位系统。 文章中涉及的任何代码都参考Linux v4.4。 Windows系统是采用软中断init 0x2e来执行系统调用。 Sysenter Based System Call Mechanism in Linux 2.6 Anatomy of a system call, part 1 Anatomy of a system call, part 2 Computer Systems: A Programmer’s Perspective 2ed, chapter 8 Exceptional Control Flow What are vdso and vsyscall?{.question-hyperlink} https://en....

February 29, 2016

LKD读书笔记:Linux的系统调用

LKD(Linux内核设计与实现) Chapter 5 System Calls序言 在远古的时候,所有的程序都是对等的,大家运行在相同的CPU权限下。那时候CPU计算能力很弱,也没有多个核可以同时运行多个程序。程序之间基本上平等,大家都排队等待获取CPU执行时间。程序的天性都很纯良,大家只埋头干好两件事,一是自己的手头活,二是应对发生的异常。那是一个美好的世界,即使有某个程序不守规矩,做坏事破坏了整个系统,大不了断电重启,世界可以重来,美好也就恢复了。 但是后来,CPU变得强大了,计算能力突飞猛进,可以有很多核同时运行成百上千的程序。于是乎CPU吼道,“凭什么老是对它掉电重启?”。这个呼声直冲云霄,惊动了人类。人类响应的CPU的呼应,创造出了一种怪物程序,这个怪物非常野蛮,它获得了CPU的特许,运行在最高的权限下,以便帮助CPU控制的所有的资源。其他程序成了奴仆,想要从怪物程序那获取资源,就必须放低姿态,卑躬屈膝得中断自己的执行,把辛苦获得的执行时间交给怪物,换取一点可怜的资源。 若干年后,所有的程序都习惯了这个怪物的存在。怪物也有了新的名字,叫操作系统。有了操作系统,CPU再也不担心运行的程序会作怪了,于是乎CPU可以专心地去发展自己的计算能力,把自己的核数目扩大一倍又一倍。 x86系统调用 以x86系统调用为例,操作系统运行在权限最高的ring 0级别,普通程序运行在ring 3级别。操作系统提供一张表,列出它可以提供的服务。64位系统,这张表在arch/x86/entry/syscalls/syscall_64.tbl,内容如下: 所有的系统调用都提供了一个编号,如read的编号是``,write是1等等。 那么内核如何定义一个系统调用呢,以read为例(代码在fs/read_write.c): 宏SYSCALL_DEFINE3在include/linux/syscalls.h定义,最终,宏会被替换成: asmlinkage修饰符告诉编译器当前函数的所有参数均放在栈上。为什么这么做呢?从用户空间切换到内核的时候,内核本来就是要保存用户空间寄存器的内容保存到栈上,以便稍后还原的,所以那些通过寄存器传递的系统调用参数自然就在栈上了。 文章中涉及的任何代码都参考Linux v4.4。 Sysenter Based System Call Mechanism in Linux 2.6 Anatomy of a system call, part 1 Anatomy of a system call, part 2 kernel/sys.c可以找到许多系统调用的实现。 一条军规是不要随便添加系统调用,实在不行,把需要用系统调用使用的功能,改用文件系统调用实现,比如 可以添加一个设备节点,然后进行read、write和ioctl。 如果只是为了吐露信息的话,可以放到sysfs。 有些借口,例如信号量,是可以用文件描述符表示的。   

February 26, 2016

LKD读书笔记:Linux进程调度之抢占机制

LKD(Linux内核设计与实现) Chapter 4 Process Scheduling当其他任务的优先级变得比当前优先级高的时候,抢占就有可能发生。当这种优先级发生变化时,need_resched标识会被设置,内核会在合适的时候抢占当前运行任务,换其他任务运行。 need_resched会在以下几种情况被设置: 每次系统时钟到来时,调度器重新计算各任务的运行时间,那些运行时间不足的任务优先级将提高。如果当前任务不是最高优先级的任务,need_resched标识会被设置。对应的处理函数是schedule_tick()。 当一个更高优先级的任务从等待队列中被唤醒时,need_resched标识会被设置。对应的处理函数是try_to_wake_up()。 当need_resched被设置后,抢占并不会马上进行,因为内核要确保当前的状态是安全的。 基本上,一个任务会处于三种状态: 在内核任务上下文情境下,任务为了等待资源而阻塞,这时本来就是要切换到其他任务,抢占没有任何问题。 在内核任务上下文情境下,任务主动调用schedule()来调度其他任务运行。这是否安全,取决于写这行代码的人是否知道自己在做什么! 当从内核情境(中断上下文和任务上下文)切换到用户空间时,内核即将进入不活动状态,这时候可以选择任何任务在用户空间运行,抢占是安全的。 当从内核中断上下文回到内核任务上下文的时候,抢占有可能会发生。 上面所有的抢占都是在特定情形下发生,是if能抢占then抢占的思路。从2.6开始,Linux开始支持全抢占逻辑,即if没有说不能抢占then就可以抢占。每个任务的thread_info里加了一个计数器preempt_count,如果当前任务没有说自己不能被抢占(即preempt_count为0),那么抢占随时会发生。 文章中涉及的任何代码都参考Linux v4.4。 switch_to()用来切换处理器状态(恢复和保存寄存器值)。 include/asm-generic/switch_to.h (通用定义) arch/x86/include/asm/switch_to.h (X86体系结构下的定义) switch_mm()用来切换页表状态。 include/asm-generic/mmu_context.h (通用定义) arch/x86/include/asm/mmu_context.h (X86体系结构下的定义) 感谢http://asciiflow.com/提供Ascii Diagram创作服务。 http://www.makelinux.net/books/lkd2/?u=ch10lev1sec8 daf94cf87写的内核抢占。 http://kernelnewbies.org/FAQ/Preemption   

February 24, 2016

LKD读书笔记:Linux的CFS调度器之Virtual Runtime

LKD(Linux内核设计与实现) Chapter 4 Process Scheduling一个理想化的多任务CPU能够同时执行所有任务,每个任务依据优先级权重分得应得的计算资源。但这样的CPU是不存在的,只能通过某种手段模拟。为此,CFS调度器设计了一个参数叫Virtual Runtime(简称vruntime)来记录当前任务所获得的运行时间。背后的道理是,相同优先级的任务所获得vruntime应该相同,某个任务获得少了,说明该任务受到不公平对待,应给其更多调度机会。 数据结构 CFS相关的数据结构主要定义在kernel/sched/sched.h 。 单个任务的的时间统计是放在struct task_struct/struct sched_entity下: 所有可以运行的任务以vruntime为键值按小到大排序构成一个timeline。Timeline使用一个红黑树来组织,树最左边的节点为vruntime最小的任务。红黑树的根节点以及最左节点都放在CFS的runqueue结构体struct cfs_rq中: 虚拟时钟 vruntime的更新是通过update_curr函数(定义在kernel/sched/fair.c)。update_curr计算当前任务获得的执行时间,累计其vruntime。此外,update_curr还做了一个动作:执行更新min_vruntime。 min_vruntime定义在struct cfs_rq中,这是个只增不减的值,主要作用是为新进任务的vruntime提供一个参照。如果直接把新进任务的vruntime设为0,会与现有的vruntime产生较大差距,导致调度器分配过多资源给新进任务。合适的做法是给新进任务设置一个足够小的vruntime,让其能够较快被调度。但初始调度之后,就能进入常态,和其他任务一样根据公平性来调度。所以min_vruntime是用来正常化(normalize)vruntime的。 Nice权重 vruntime之所以和任务获得的真实执行时间不一样,是因为它是以Nice值为权重加权计算出来的值。累计vruntime的时候需除以权重再累加。Nice值高的任务,权重低,vruntime就累计得快;nice值低的任务,权重高,vruntime累计得慢。所以Nice值低的任务就能获得更多真实执行时间。 update_curr调用calc_delta_fair函数对vruntime的累加进行加权。权重序列以几何级数方式散开,从而保证nice值的比较优先级,即两个任务获得CPU比例只和相应nice值的差别有关系,和nice值的绝对值没有关系。   文章中涉及的任何代码都参考Linux v4.4。 Documentation/scheduler/sched-design-CFS.txt Linux Scheduler – CFS and Virtual Run Time (vruntime) Linux Scheduler – CFS and Nice Professional Linux® Kernel Architecture Linux进程调度(1):CFS调度器的设计框架

February 14, 2016

LKD读书笔记:Linux的CFS调度器设计理念

LKD(Linux内核设计与实现) Chapter 4 Process Scheduling以往在调度器的设计中,时间片(timeslice)是一个很重要的概念,它决定了一个任务能够运行多长时间而不被抢占。其机制是这样的,每当系统时钟中断来临时,调度器从当前任务的时间片中减去一个时钟周期,直至时间片耗光,这时候当前任务就会被抢占,换成其他任务执行。所以如何划分和分配时间片成了调度器设计上最重要的问题。 为了解决上面的问题,以往的调度器把动态优先级(nice值)和时间片绑定在一起,高优先级获得长时间片,低优先级获得短时间片,调度器优先调度具有时间片长的任务。但是,如果分配不合理,时间片长的任务并不一定是当下最需要CPU时间的任务。 2.6.23版本引入了CFS(Complete Fair Scheduler),引入了公平性的概念。 CFS假设一个理想化的CPU,可以同时运行所有任务。以动态优先级作为权重,这些同时运行的任务获得其应得的CPU计算能力的一部分。 现实中CPU(的一个核)单次只能运行一个任务,那么CFS调度器如何模拟这个理想化的CPU呢? 计算每个任务的应得运行时间,也就是CPU计算能力份额。计算时以动态优先级为权重,将当前所有可运行任务纳入考量,得出单个任务的应得运行时间。 以任务实际运行时间和应得运行时间差距为调度依据,差距大的优先运行。 上述的原则大大简化了调度决策过程,保证了公平性,顺利解决了以往以时间片长短作为调度依据而导致的时间片分配不均的问题。 同时还创新性地解决了交互性问题,如果一个任务长时间处于I/O等待状态,那么这个任务的实际运行时间和应得运行时间差距就会加大,当被唤醒的时候,它就会被优先调度。以往,交互性问题只能在I/O任务被唤醒的时候分配更多时间片来提升它的优先级。 模拟理想化CPU的最大阻碍来自于切换任务的代价。切换不仅会花费时间,还有可能影响cache的性能,是一个不得不考虑的问题。为此CFS设了一个最小粒度,即一个任务至少能够运行1ms而不被强占。 CFS替代了之前的O(1)调度器,成为SCHED_OTHER(偏向交互性)策略下的默认调度器。 文章中涉及的任何代码都参考Linux v4.4。 Documentation/scheduler/sched-design-CFS.txt 为了改进O(1),内核开发者做了许多尝试,其中最引人注目的是RSDL(Rotating Staircase Deadline Scheduler),它引入了排队理论中的公平性调度原则,CFS正是借鉴了这一设计。   

February 6, 2016

LKD读书笔记:Linux进程调度之nice值和时间片

LKD(Linux内核设计与实现) Chapter 4 Process Scheduling传统的调度器设计,一个任务能够运行多长时间而不被抢占,是根据分配给这个任务的时间片决定的。优先级高的任务分配的时间片就长,优先级低的任务分配的时间就短。 系统根据每个进程的nice值来决定如何分配时间片。但nice值的范围只有-20到+19,共40个离散的值。如何用这40个值来给那么多运行中的任务定优先级、分时间片?能保证每个任务得到应得的运行时间吗? 上面问题的答案显然是不行的。现在计算机的性能越来越好,同时运行的任务越来越多,不同的任务对CPU时间有不同的需求,40个nice值已经不能满足需求了。 按nice值分配时间片的方法有以下几个缺点: 考虑到CPU切换任务需要花费一定的代价(消耗CPU时间,影响Cache性能),优先级最低的nice值(+19)对应的时间片就无法设的太小。假设nice+19的时间片设为5ms,如果系统当前只有两个nice+19的任务同时运行,这两个任务就会每5ms切换一次,这种情况下本不需要如此快切换。 优先级最高的nice值(-20)对应的时间片不能设置的太大。假设nice-20的时间片是100ms,当某个nice-20的任务运行时,突然一个交互性任务需要运行(比如响应键盘输入),这个交互性任务可能最长需要等待100ms才能获得CPU时间。 无法使用比较优先级。假设任务A、B的nice值是0、1,它们获得的时间片可能是100ms,95ms;如何任务A、B的nice值是18,19,它们获得的时间片可能是10ms,5ms。虽然两种情况下时间片的差别都是5ms,但是第二种情况下任务A获得的CPU时间远超过第一种情况(A的10ms对B的5ms)。 时间片依赖于系统时钟的精度(系统时钟可以是1ms~10ms)。假设系统时钟是10ms,那么时间片的最小值只能是10ms,而且时间片之间的差距最小值也只能是10ms。如果一个任务的应得运行时间是1ms或11ms,但实际得到的则是10ms或20ms。对一个任务分配超过应得的运行时间,就会对系统中其他任务造成不公平。 另外一种不公平现象会发生在交互性任务的调度上。为了保证系统的交互性(例如让大家敲键盘的时候不感觉到延迟),调度器一般会偏向交互性任务,在调度的时候优先保持它们的低延迟。这回造成一种情况,当一个交互性任务刚用完它的应得时间片后,突然来了一个交互请求(一个键盘输入),调度器很可能重新给这个任务分配新的时间片。对一个任务太好,就是对其他任务不太好,公平性就无法保证。 在2.6.23之前,Linux使用的是O(1)调度器。为了缓和上面的问题,O(1)调度器的代码变得异常复杂。虽然修修补补改进了O(1)的性能,但是根本的问题没有解决,即按优先级来分配时间片既会把任务可以选择的时间片限定在某一范围,又会导致对单一任务分配超过应得的时间片,从而影响总体的公平性。 这篇到此为止,以后会介绍2.6.23引入了CFS调度器,不再把nice值直接映射到时间片了,一举解决了上述问题。 文章中涉及的任何代码都参考Linux v4.4。 Documentation/scheduler/sched-nice-design.txt   

February 5, 2016

LKD读书笔记:Linux进程调度初览

LKD(Linux内核设计与实现) Chapter 4 Process Scheduling作为一个多任务抢占式操作系统,Linux调度器需要决定: 在当下选择哪个任务运行 是否抢占正在运行的任务 这是个相当复杂的问题,不同的系统有不同的偏向性。比如一些系统偏向实时性的任务调度,要求任务在执行时间上有确定性;另外一些系统则偏向吞吐量,希望能执行更多任务,最大化CPU使用率;还有一些系统注重交互性,比如桌面系统。 对此,单一的调度策略显然不能满足所有偏向性。Linux支持不同调度策略: SCHED_FIFO (软实时,基于优先级) SCHED_RR(软实时,基于优先级+时间片) SCHED_OTHER/SCHED_NORMAL (非实时,交互性任务和吞吐性任务并重) SCHED_BATCH ( 非实时,偏向批处理任务,出现于2.6.16) SCHED_IDLE (非实时,偏向驻留后台的低调任务,出现于2.6.23) SCHED_FIFO/SCHED_RR Linux为了优先满足实时性任务,设计了两个高优先级的调度策略SCHED_FIFO和SCHED_RR,这两个调度策略下的任务可以抢占其他策略下的任务。 SCHED_FIFO和SCHED_RR采用静态优先级机制,高优先级的任务抢占执行。不同的地方是SCHED_FIFO需要高优先级的任务主动放弃执行之后其他任务才有机会执行;而SCHED_RR策略多一个时间片的概念,当一个任务用完时间片之后,其他同优先级的任务可以有机会执行。 相应的代码在kernel/sched/rt.c。 SCHED_OTHER/SCHED_NORMAL SCHED_OTHER,顾名思义是用来调度除了实时策略之外的任务,即任务的静态优先级为0。作为桌面系统或者服务器系统的,大部分的任务是运行在这个调度策略之下。 这个策略要求调度器既有交互性又有吞吐量,要兼顾两种极端类型的任务,一是I/O-Bound的,另一种是Processor-Bound的。前一种任务大部分时间都在等待I/O操作,但是在唤起时需要更快地被调度到,也就是说这种任务希望延迟较低;后一种任务则希望更多地占据CPU时间,不被打断。 SCHED_OTHER的任务具有动态优先级,调度器根据任务的CPU使用情况来调整动态优先级。任务的动态优先级受相应的nice值影响,nice值范围从-20到+19,值越大优先级越低。 相应的代码在kernel/sched/fair.c。 SCHED_BATCH 这个策略是2.6.16新增的,和SCHED_OTHER一样静态优先级为0,用来调度侧重吞吐量的任务。 相应的代码在kernel/sched/fair.c。 SCHED_IDLE 这个策略是2.6.23新增的,和SCHED_OTHER一样静态优先级为0,用来调度CPU需求极低的任务。 相应的代码在kernel/sched/idel.c。 相应的系统调用 sched_setscheduler, sched_getscheduler – set and get scheduling policy/parameters (设置调度策略) sched_get_priority_max, sched_get_priority_min – get static priority range(设置静态优先级) sched_setparam, sched_getparam – set and get scheduling parameters ( nice – change process priority (设置影响动态优先级的nice值) getpriority, setpriority – get/set program scheduling priority (设置影响动态优先级的nice值,功能比nice强大) sched_rr_get_interval – get the SCHED_RR interval for the named process (设置SCHED_RR策略的时间片长度)   文章中涉及的任何代码都参考Linux v4....

February 3, 2016

LKD读书笔记:Linux进程管理之task_struct初览

作为多任务操作系统,Linux管理着许多进程。在内核里面,进程的context由一个struct task_struct表示,定义在头文件<linux/sched.h>中。在内核中task既是进程,进程既是task。 task用pid标识,pid为1初始的init进程的task_struct时是静态分配的,叫做init_task。 linux提供了一个C语言的宏current用来获取当前task的task_struct,current的实现是CPU相关的。 task_struct作为可以运行的实体,它的运行时状态由state属性表示,TASK_RUNNING表示task处于可运行状态,可用set_task_state(task, state)来更改。task的终止状态由exit_state表示,EXIT_DEAD表示task死亡。 在tasks管理方面,首先所有的tasks构成一个环形链表,task_struct的tasks属性是该链表的节点,可以用以下方法遍历tasks: 此外,tasks又以树形的方式组织,属性parent, children, sibling用于此目的。 LKD(Linux内核设计与实现) Chapter 3 Process Management Linux头文件 <linux/sched.h> 机制: pid的最大值可以由/proc/sys/kernel/pid_max控制,默认是32768。 实现: 进程由fork()创建,代码在kernel/fork.c。 fork()具有Copy-on-write以及Child-runs-first等语义。 vfork和fork略有不同,vfork不拷贝page table,父进程阻塞直到子进程退出或者执行execve()。 当父进程先于子进程退出时,kernel要为子进程重新找一个父进程,相关的函数是find_new_reaper()和reparent_thread()。

January 19, 2016

Linux的僵尸进程

在Linux系统上如果一个进程死亡,这个进程就会变成僵尸进程,需要该进程的父进程去给其收尸,这个过程有一个很形象的称呼,叫做”reaping the zombies”。 之前有个疑问,为什么要做僵尸进程这样的设计?思来想去,这应该是父子进程通信的一种方式吧,子进程死亡后留下僵尸进程,留给父进程一个机会去获取子进程的状态。 下面是做实验时间,写一个简单的Python2.7脚本来产生僵尸进程: 执行上面的脚本,会产生一个僵尸进程。因为父进程处于signal.pause()状态时子进程退出了,父进程没有来得及给子进程收尸。一次执行的输出是: 我们用ps u命令来查看进程列表,可以看到: PID为457的子进程成为僵尸进程,被标志为Z+。 避免僵尸进程 既然僵尸进程的存在是为了让父进程有机会捕获子进程的退出状态,那么在机制上它应该是可以避免的。 Stack Overflow上的这篇文章Why zombie processes exist?{.question-hyperlink}给出了几种方法,其中一种是直接告诉系统,你不想关心子进程状态,压根不要产生僵尸进程。方法是忽略父进程的SIGCHLD信号,这样子进程退出后就不会存留僵尸进程。 修改一下前面的Python脚本,忽略SIGCHLD信号: 运行输出: 用ps u查看进程状态,发现原先的僵尸进程已经不存在了: 清理僵尸进程 综上所述,僵尸进程的存在是因为子进程退出了,而父进程没有收尸。这其实是程序设计上的失败。如果产生这种情况,只能通过杀死那些父进程,让init进程接管所有的僵尸进程为其收尸了。 How to kill zombie process{.question-hyperlink}给了一个非常好的命令行: 完。

December 1, 2015