1,Linux中的进程管理
1)进程和线程
进程是资源分配的最小单位。
线程是操作系统调度执行的最小单位。
进程和线程是程序运行时状态,是动态变化的,进程和线程的管理操作(比如,创建,销毁等)都是由内核来实现的。Linux中的进程于Windows相比是很轻量级的,而且不严格区分进程和线程,线程是一种特殊的进程。
进程提供2种虚拟机制:虚拟处理器和虚拟内存
每个进程有独立的虚拟处理器和虚拟内存,每个线程有独立的虚拟处理器,同一个进程内的线程有可能会共享虚拟内存。
内核把进程的列表存放在任务队列(task list)中(双向循环链表),链表的每一项类型为task_struct,我们称之为进程描述符(process descriptor)。进程的信息主要保存在task_struct中(位于 include/linux/sched.h),如下图所示:
2)进程的生命周期
进程各个状态之间的转化构成了进程的生命周期:
进程有五种进程状态:
除了图片上面的三种还有,_TASK_TRACED和_TASK_STOPPED。
3)进程的创建
Linux中创建进程分2步:fork()和exec()。
1, fork(): 通过拷贝当前进程创建一个子进程 (实际上最终是通过clone( ) )。
2, exec(): 读取可执行文件,将其载入到地址空间中运行 (是一个系统调用族)。
创建的流程:
1 调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程相同。
2 check新进程(进程数目是否超出上限等)
3 清理新进程的信息(比如PID置0等),使之与父进程区别开。
4 新进程状态置为 TASK_UNINTERRUPTIBLE
5 更新task_struct的flags成员。
6 调用alloc_pid()为新进程分配一个有效的PID
7 根据clone()的参数标志,拷贝或共享相应的信息
8 做一些扫尾工作并返回新进程指针
9 创建进程的fork()函数实际上最终是调用clone()函数。
创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。
比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)
创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)。
4)进程的中止
发生在进程调用exit()系统调用时
和创建进程一样,终结一个进程同样有很多步骤:
子进程上的操作,靠do_exit()完成->定义于(kernel/exit.c)
1 设置task_struct中的标识成员设置为PF_EXITING
2 调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
3 调用exit_mm()释放进程占用的mm_struct
4 调用sem__exit(),使进程离开等待IPC信号的队列
5 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
6 把task_struct的exit_code设置为进程的返回值
7 调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE
8 切换到新进程继续执行。
子进程进入EXIT_ZOMBIE之后,虽然永远不会被调度,关联的资源也释放 掉了,但是它本身占用的内存还没有释放,比如创建时分配的内核栈,task_struct结构等。这些由父进程来释放。父进程上的操作(release_task)
父进程受到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。
从上面的步骤可以看出,必须要确保每个子进程都有父进程,如果父进程在子进程结束之前就已经结束了会怎么样呢?
子进程在调用exit_notify()时已经考虑到了这点。如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)。
5)Linux中的进程调度
现在的操作系统都是多任务的,为了能让更多的任务能同时在系统上更好的运行,需要一个管理程序来管理计算机上同时运行的各个任务(也就是进程)。这个管理程序就是调度程序,它的功能说起来很简单:
1,决定哪些进程运行,哪些进程等待
2, 决定每个进程运行多长时间
此外,为了获得更好的用户体验,运行中的进程还可以立即被其他更紧急的进程打断。总之,调度是一个平衡的过程。一方面,它要保证各个运行的进程能够最大限度的使用CPU(即尽量少的切换进程,进程切换过多,CPU的时间会浪费在切换上);另一方面,保证各个进程能公平的使用CPU(即防止一个进程长时间独占CPU的情况)。
调度功能就是决定哪个进程运行以及进程运行多长时间。
决定哪个进程运行以及运行多长时间都和进程的优先级有关。为了确定一个进程到底能持续运行多长时间,调度中还引入了时间片的概念。
(1)关于进程的优先级
1, 进程的优先级有2种度量方法,一种是nice值,一种是实时优先级。
2 ,nice值的范围是-20~+19,值越大优先级越低,也就是说nice值为-20的进程优先级最大。
3, 实时优先级的范围是0~99,与nice值的定义相反,实时优先级是值越大优先级越高。
4, 实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。
5 ,实时优先级高于nice值。
6 ,一个进程不可能有2个优先级。
(2)关于时间片
有了优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms。
(3)调度实现的例子
假设系统中只有3个进程
ProcessA(NI=+10),ProcessB(NI=0),ProcessC(NI=-10),NI表示进程的nice值,时间片=10ms。
- 调度前,把进程优先级按一定的权重映射成时间片(这里假设优先级高一级相当于多5msCPU时间)。假设ProcessA分配了一个时间片10ms,那么ProcessB的优先级比ProcessA高10(nice值越小优先级越高),ProcessB应该分配105+10=60ms,以此类推,ProcessC分配205+10=110ms。
- 开始调度时,优先调度分配CPU时间多的进程。由于ProcessA(10ms),ProcessB(60ms),ProcessC(110ms)。显然先调度ProcessC。
- 10ms(一个时间片)后,再次调度时,ProcessA(10ms),ProcessB(60ms),ProcessC(100ms)。ProcessC刚运行了10ms,所以变成100ms。此时仍然先调度ProcessC。
- 再调度4次后(4个时间片),ProcessA(10ms),ProcessB(60ms),ProcessC(60ms)。此时ProcessB和ProcessC的CPU时间一样,这时得看ProcessB和ProcessC谁在CPU运行队列的前面,假设ProcessB在前面,则调度ProcessB。
- 10ms(一个时间片)后,ProcessA(10ms),ProcessB(50ms),ProcessC(60ms)。再次调度ProcessC
- ProcessB和ProcessC交替运行,直至ProcessA(10ms),ProcessB(10ms),ProcessC(10ms)。这时得看ProcessA,ProcessB,ProcessC谁在CPU运行队列的前面就先调度谁。这里假设调度ProcessA。
- 10ms(一个时间片)后,ProcessA(时间片用完后退出),ProcessB(10ms),ProcessC(10ms)。
- 再过2个时间片,ProcessB和ProcessC也运行完退出。
这个例子很简单,主要是为了说明调度的原理,实际的调度算法虽然不会这么简单,但是基本的实现原理也是类似的:
1)确定每个进程能占用多少CPU时间(这里确定CPU时间的算法有很多,根据不同的需求会不一样)
2)占用CPU时间多的先运行。
3)运行完后,扣除运行进程的CPU时间,再回到 1)。
Linux上的调度算法是不断发展的,在2.6.23内核以后,采用了“完全公平调度算法”,简称CFS。CFS算法在分配每个进程的CPU时间时,不是分配给它们一个绝对的CPU时间,而是根据进程的优先级分配给它们一个占用CPU时间的百分比。