进程管理
概念:
- 程序:本身并不是进程,程序指的是可执行的程序。
- 进程:进程是处于执行期的程序以及相关资源的总称。
进程并不仅仅局限于一段可执行程序代码,还要包括其他资源,如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程。
- 线程:是指进程中活动的对象。
每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器,内核调度的对象是线程,而不是进程,线程只不过是特殊的进程。
- 僵死进程: 子进程退出后,其父进程调用wait()回收前的子进程状态。
- 孤儿进程: 父进程比子进程先退出,子进程没有找到父进程前的子进程状态。
- 进程上下文: 内核"代表进程执行"的时候。
程序一般在用户空间执行,当程序调用系统调用或者出发了某个异常,进程就会陷入内核空间,此时由内核代表进程执行程序,此时就处于进程上下文中。
进程的生命周期:
如上,Linux进程有五种状态:
TASK_RUNNING
(运行)、TASK_INTERRUPTIBLE
(可中断)、TASK_UNINTERRUPTIBLE
(不可中断)、__TASK_TRACED
(被其他进程跟踪的进程)、__TASK_STOPPED
(停止)
进程创建
linux进程创建由两个函数完成:fork()和exec()。
fork():通过拷贝当前进程创建一个子进程。
fork()创建一个子进程,子进程与父进程的区别仅仅在于PID、PPID和某些资源和统计量(例如挂起的信号,锁等)。
exec(): 复制读取可执行文件并将其载入地址空间开始运行
exec是所有exec()函数簇的一员,其他的比如:execlp()、execle()、execvp()等。
写时拷贝:
传统的fork()是将所有的资源都复制给新创建的进程,导致效率低下,所以linux提供了一种写时拷贝页的机制。
这是一种可以推迟甚至免除拷贝的技术。
fork()完之后父进程和子进程共享(只读共享)一个地址空间,只有当需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。 所以fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
fork()进程创建过程:
Linux是通过clone()系统调用实现fork()的,clone()最终会调用do_fork()。
do_fork()完成了创建中的大部分工作,然后调用copy_process()函数,
copy_process()步骤:
- 调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程相同。
- 检查进程数目是否超出上限。
- 清理新进程的信息(比如PID置0等),使之与父进程区别开。
- 新进程状态置为 TASK_UNINTERRUPTIBLE,使其不会被运行。
- 更新task_struct的flags成员。
- 调用alloc_pid()为新进程分配一个有效的PID
- 根据clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、进程地址空间等。
- 做一些扫尾工作并返回新进程指针
copy_process()成功返回后,新创建的子进程被唤醒并让其投入运行。
除了fork()之外,还有vfork()和__clone()都会调用clone()实现创建子进程功能。 其中vfork()除了不拷贝父进程页表项外,而且vfork()可以觉得让子进程先运行,其他和fork()功能相同。
进程终结过程
当一个进程终结时,内核必须释放它所占有的资源,并告诉父进程,通常情况下是调用exit()
或_exit()
终结进程的。 区别如下:
它们最终都会调用do_exit()函数,其步骤为:
- 设置task_struct中的标识成员设置为PF_EXITING
- 调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
- 调用exit_mm()释放进程占用的mm_struct
- 调用sem__exit(),使进程离开等待IPC信号的队列
- 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
- 把task_struct的exit_code设置为进程的返回值
- 调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE
- 调用shedule()切换到新进程继续执行, do_exit()永不返回。
至此,调用完do_exit()之后,与进程相关联的所有资源都被释放掉了,变成了僵死进程,但是此时进程还保留了需要向父进程提供的信息:进程描述符,创建时分配的内核栈,task_struct结构等,需要由父进程释放。
父进程处理已退出子进程的资源是通过调用wait()函数来实现的,wait()会调用一个系统调用wait4()实现。
wait()会挂起调用它的进程,知道其中一个子进程退出,此时会返回该子进程的PID。
最终释放子进程进程描述符由release_task()完成:
- 最终调用detach_pid()从pidhash上删除进程,同时从任务列表中删除该进程。
- _exit_signal()释放目前僵死进程的所有剩余资源。
- 若该进程是线程组的最后一个进程,且领头进程已经死掉,则通知僵死的领头进程的父进程。
- 释放进程内核栈和所占的页,并释放掉slab高速缓存。
至此,子进程就完全被释放掉了。
上面是子进程先退出的处理过程,若父进程比子进程(孤儿进程)先退出了呢?
Linux有专门的机制来保证子进程能找到一个新的父亲,否则孤儿进程在退出时会永远成为僵死进程。
解决方法:现在当前线程组找到一个线程作为父亲, 如果没有则让第一个进程init作为它们的父进程。
在do_exit()
中会调用xit_notify()——》forget_original_parent()-》find_new_reaper()
来寻找父进程。
init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。