Linux进程管理
Linux内核基于Minix编写,现在的都是Linux发行版,是内核的自定义。
现代操作系统允许一个进程有多个执行流,即在相同的地址空间中可执行多个指令序列。每个执行流用一个线程表示,一个进程可以有多个线程。
Linux具体实现比较特别,Linux中严格来说是没有线程的,而是使用轻量级进程实现对多线程应用程序的支持,一个轻量级进程就是一个线程。
本章内容:
Linux进程组成
进程有两种状态,用户态和核心态。
这两种状态的根本区别在于切换了段地址。用户态的时候用的是用户栈,而核心态用的是核心栈。
状态的切换以及进程的切换都需要保存信息,其中就用到进程描述符
task_struct结构如下,图中仅列出关键的几个部分,实际上有更多:
- thread_info是进程的关键信息,又叫小描述符
- 略
每个进程都在内存中有一个进程核心栈,其和thread_info是放在一起的,总共占2页空间,核心栈在一端,thread_info在另一端。
task_struct通过thread_info指针访问核心栈,核心栈通过thread_info反向访问task_struct,是双向的。
具体到操作系统,操作系统通过esp获取thread_info地址,然后通过thread_info获取task_struct(PCB),即操作系统通过esp获取PCB信息。尤其是进程刚从用户态切换到核心态时,其核心栈为空,只要将栈顶指针减去8k,就能得到thread_info结构的地址。
- 可运行状态=运行态+就绪态
- 阻塞态分的比较细,下面逐一列出:
- 可中断的等待状态:进程睡眠等待系统资源可用或收到一个信号后,进程被唤醒。
- 不可中断的等待状态:进程睡眠等待一个不能被中断的事件发生。如进程等待设备驱动程序探测设备的状态。
- 暂停状态;
- 跟踪状态 ;
- 僵死状态 ;
- 死亡状态
Linux进程链表
Linux的链表都是list_head类型列表
全局的链表有:
- 所有进程链表tasks:链表头是0号进程,双向链表
- 可运行进程(TASK_RUNNING)链表run_list:按照它们的优先级可构建140个可运行进程队列(系统有140个优先级,非常细致)
- 等待进程链表。互斥等待访问临界资源的进程;非互斥等待的进程,所有进程都被唤醒wait_queue_t
这两个链表,每一个PCB都有:
- 子进程链表children
- 兄弟进程链表sibling
pid实际上是一种哈希(TODO)
Linux进程控制
用户进程创建与撤销
Linux有三个创建进程的函数:
- fork。采用写时复制技术,复制全部资源,根据返回pid判断是否是子进程,不阻塞。
- vfork。共享资源,且阻塞父进程。
- clone。轻量级进程函数,用于实现Linux线程,可以选择共享哪些数据。
要想实现共享+并行,需要用fork+共享内存区+PV操作实现。
进程撤销:
- exit()系统调用只终止某一个进程/线程。
- exit_group()系统调用能终止整个线程组。
如果只将主进程exit,子进程保留,子进程就会变成孤儿进程。 此时会给孤儿分配养父,即init进程(1号,监控用户态进程),init进程定期会处理僵死的子进程。
0,1,2号进程
前面是用户空间进程的创建,内核有另一套函数。
内核空间的线程创建create_ kthread () / kthread_run()。内核里只有线程,因为内核本身就是root权限,不需要进行数据隔离。出于好听的原因,人们又喜欢把内核线程称作进程。
内核线程的任务一般是执行周期性执行的任务,如刷新磁盘高速缓存;交换出不用的页;维护网络连接等任务。相对的,用户线程高度自定义,不具有周期性。
- 0号进程是一切进程的根。0号进程就是一个内核线程,0号进程是所有进程的祖先进程,又叫idle进程或叫做swapper进程。每个CPU都有一个0号进程。
- 1号进程(init)是用户态进程的根。是由0号进程创建的内核线程init,负责完成内核的初始化工作。在系统关闭之前,init进程一直存在,它负责创建和监控在操作系统外层执行的所有用户态进程。
- 2号进程(kthreadd)负责创建内核线程。
使用ps查看进程信息。-eo自定义显示信息,显示pid,ppid,command信息。
可以看到,没有显示0号进程,而1号进程负责init任务与用户态进程创建,其父进程是0号进程,2号进程负责内核线程创建。这里显示的其他进程都是内核线程,任务各不相同,其父进程都是2号进程。
Linux进程切换
用户态进程之间可以切换。用户态进程到核心态也需要切换。这两种切换执行的思路类似:
这里需要注意,进程切换只能发生在核心态,毕竟用户怎么能掌握计算机运行的调度权呢?那问题来了,从核心态切到用户态很自然,那用户态怎么切到核心态呢?这就需要触发中断,发送一个信号给核心,然后核心再进行切换。
所以一个切换操作分如下流程:
- 需要先将用户态寄存器信息保存,送到核心栈
- 还有一些寄存器值被送到PCB的thread_struct 的 thread字段中(硬件上下文)
- 最后通过核心态进行进程切换,先切页目录表(换一个页目录段,换一个页表TODO),再切核心栈与硬件上下文(恢复寄存器)
Linux进程调度
Linux,无论是用户态还是核心态,都是可抢占的。 为了保证抢占的合理,需要搭配动态优先级。
进程调度类型:
- 先进先出的实时进程,时间片轮转的实时进程。基本优先数为1~99,优先级较高,费时较少
- 普通的分时进程。分时进程和批处理进程的基本优先数为100~139
实时进程调度时机:
- 被抢占了。出现了更高优先级的实时进程
- 走不动了。进程执行了阻塞操作,或者干脆停止运行或被杀死
- 自愿放弃。进程调用了sched_yield()自愿放弃处理机
- 轮转的实时系统中,时间片用完。
进程调度需要用到特殊的数据结构,其实就是前面的进程链表,是一维数组runqueues(TODO,TODO到底是一维数组还是链表?)。
一个CPU有140级全局可运行进程链表。
在时间片轮转系统中,还可以再分两类,一类是活动进程链表,一类是过期进程链表,各有140个队列。当活动进程都过期后,过期进程才可运行。避免低优先级进程没有机会运行(进程饥饿)
刚开始优先级动态计算复杂度比较高,所以引入了公平调度算法。
内核同步
不同内核线程使用一个内核,所以对于互斥资源就需要进行同步控制。核心思路就是保证临界区的同一时间只对应一个内核控制路径(?TODO)
同步方法很多,不止有信号量。
Linux储存器管理
进程地址空间的管理
内核虚空间
32位机器的进程寻址空间为4G。
进程的私有空间是3G,剩下1G是内核虚空间。
1G虚拟空间的前896M对应物理内存的前896M。前896MB的物理地址等于内核虚地址减去0xc0000000(3G)后128MB的虚拟空间比较特殊,是固定的分区,与用户。
用户空间进程管理
注意,这里针对的是3G的内存空间,是进程管理。而核心都是线程,不存在下面的管理机制。
针对3G的用户空间,Linux的管理机制如下图: 看着比较庞大,我们大致捋一下,你可能现在看不懂,但是看完后面两个结构的具体描述再回来看也是OK的:
- 从右往左看:实际上,一个进程的虚拟内存逻辑上是连续的,分为若干个区域。每一个区域都有一个vm_area_struct对应,这些vm_area_struct是以链表结构+红黑树结构组织的。
- 从左往右看:一个进程PCB通过一个mm_struct对虚拟空间进行宏观管理
- mm_struct和vm_area_struct就像是总分的关系一样,具体有什么联系?mm_struct指向vm_area_struct链表的头结点,指向其红黑树的根节点。而每一个vm_area_struct里面都有一个指针指向同一个mm_struct,用于快速返回mm_struct。
vm_area_struct
struct vm_area_struct {
struct mm_struct * vm_mm; 虚拟内存描述符,指向其对应的mm_struct
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
struct rb_node vm_rb;
struct file * vm_file; 映射文件时指向文件对象
……
}
结构里同时有红黑树指针和链表指针,说明vm area struct是同时具有两种组织方式。
红黑树是一种特殊的平衡二叉树,满足红黑树规则的n节点树,高度最多为
2
×
l
o
g
(
n
+
1
)
2\times log(n+1)
2×log(n+1)
mm_struct
内核线程不拥有mm_struct(本身就是线程,mm struct是针对进程的) 。
struct task_struct
{
…
struct mm_struct *mm;
…
}
struct mm_struct {
struct vm_area_struct *mmap;
struct rb_root mm_rb;
pgd_t *pgd;
atomic_t mm_users;
atomic_t mm_count;
struct list_head mmlist;
unsigned long start_code, end_code;
……};
结构里有指向vm area struct链表的头指针,也有指向红黑树结构的指针。
说一下mm_users mm_count这两个计数器。
- mm_users记录共享mm_struct的轻量级进程数。初值为1(主线程),增加一个线程就+1(TODO)
- mm_count记录内核线程使用数。初值为0,mm若把mm_struct暂时借给一个内核线程使用,则mm_count值增1。
进程结束,mm_users和mm_count都为0时,这个mm_struct才能被释放。
物理内存管理
物理内存是连续的(假设4G)
页框0给bios用,从0x000a0000到0x000fffff(1M空间)给BIOS例程用。
Linux管理页框的时候,会跳过前1M的空间(BIOS空间,对应RAM)。剩下的页框,大小为4KB,每一个页框都有一个页框描述符,struct page。这些struct page以结构数组的形式放在mem_map数组中。
struct page
这个结构,mapcount是页框号。其余的字段,要注意private字段,和伙伴系统有关(后面)。还有mapping字段,和页高速缓存的核心数据结构,与文件的inode有关。
struct page {
unsigned long flags;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;用于页高速缓存
pgoff_t index; 在页高速缓存中以页为单位偏移
struct list_head lru; 链入活动页框链表或非活动..
void *virtual;
};
内存管理区
整个4G内存被分成3块zone:
- ZONE_DMA:包含低于16MB的常规内存页框。用于对老式的基于ISA设备的DMA支持。0~15MB
- ZONE_NORMAL:包含高于16MB且低于896MB的常规内存页框。16MB~895MB,内核可以直接访问,用虚拟地址-3G即可得到物理内存。
- ZONE_HIGHMEM:包含从896MB-4G的高端物理页框。内核不能直接访问,后128+3G需要通过页管理机制寻址。
也就是说,大部分内存区域仅仅被一个zone结构管理:
这个结构里又出现了伙伴系统。
struct zone {
unsigned long free_pages; 空闲页框数
struct per_cpu_pageset pageset[NR_CPUS];
struct free_area free_area[11];
struct list_head active_list;
struct list_head inactive_list;
…….};
分区页框分配器与伙伴系统
free_area是11个长度的数组。对应
2
0
?
2
10
2^0-2^{10}
20?210长度。有的程序会需要连续的物理空间,而这个数组就负责统计整个zone里的连续物理空间,记录在数组中。
具体来说,比如数组中的8号位,实际上是一个链表头指针。指向一个链表,每一个链表节点都是起始页框描述符,这意味着这个链表对应着n段长度为8的连续物理空间。
而这里也揭示了private有什么用,private表示的是2的幂,如果是4就是16长度,3就是8长度。
伙伴系统就是基于分区页框分配器的。
假设要请求一个具有8个连续页框的块,该算法先在8个连续页框块的链表中检查是否有,如果没有,就在16个连续页框块的链表中找,如果找到,就把这16个连续页框分成两等份,一份用来满足请求,另一份插入到具有8个连续页框块的链表中;
如果在16个连续页框块的链表中没有找到,就在更大的块链表中查找,比如32找到了,就先切16,插入到16链表中,然后再把剩下的再切,把8插入到8链表中,最后剩下的8个连续页框就分配出去。
回收的时候,会检查长度,如果长度从8变成16,就合并,插入16中。
slab小内存分配器
伙伴系统以页框为单位,分配大块内存。有一些file对象或者各种描述符需要大量的小内存,这就是slab分配器的作用。
slab分配器先批发一些连续页框,常驻内存,构成高速缓冲区,缓冲区内部进行细粒度分配。常驻内存以空间换时间,减少了内存分配初始化销毁释放的代价。
slab分配器生成的每个高速缓存存储一种类型的对象。高速缓存由一连串的slab构成,每个slab包含了若干个同类型的对象。这种细粒度的切分也是slab的特点。
虚拟地址转换
从一个虚拟地址到最后的物理地址,要经过多级转换。
首先,通过段基址+段偏移,获得32位线性地址,然后将32位线性地址通过分页部件转换成物理地址。
无论是页目录表项还是页表项,结构都是一样的。一个页表项,不仅仅包含地址,还有很多字段:
传统的页表项字段且不说,虚拟内存的页表项增加了一些字段:
- 物理地址20位。
- Present。标志页表是否在内存中。
- Accessed访问位,用于页面置换算法。
- Dirty位,写操作标记位,用于交换。
盘交换区与页面置换
Linux中,有一个磁盘交换区。可以理解为内存与磁盘之间的一种缓存。交换区可以直接作为一个磁盘分区,这种交换区就只有一个子区(swap分区),也可以以文件形式存在,但是这种就会被切分为多个物理块(文件会被离散储存)。
盘交换区由若干页槽组成,页槽大小和内存的页大小对应,为4K。盘交换区的第一个页槽存放交换区的整体信息,其他页槽用于交换。发生交换的时候,内核尽量把换出去的页放在相邻页槽中,减少后续磁道寻道时间。
缺页中断发生在这些情况下:
- 没访问过。该页从未被进程访问过,且没有相应的内存映射。
- 访问过,但被交换到交换区了。该页已被进程访问过,但其内容被临时保存到磁盘交换区上。
- 休眠中。该页在非活动页框链表中。
- 被锁了。该页正在由其它进程进行I/O传输过程中,这种只能阻塞。
发生缺页中断时,会先去交换区找,交换区找不到就会去磁盘去调。
页面置换策略是LFU(Least Frequently Used)。注意这和LRU不同,R是Recently,而这个是Friquently,这个算法会统计最近调用一页的频数,频数最少的就被交换出去。
|