Linux进程管理
Linux进程管理
1、进程
进程是执行期的程序,包含可执行代码、打开的文件、挂起的信号、内核内部数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个线程以及存放全局数据的数据段等。
线程是在进程中活动的对象,每个线程拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核的调度对象为线程。在Linux实现中,线程是一种特殊的进程。
进程提供两种虚拟机制:虚拟处理器以及虚拟内存。虚拟处理器让进程觉得自己在独享处理器,而虚拟内存则让进程在分配和管理内存时觉得拥有系统所有的内存资源。线程之间可以共享虚拟内存,但每个都拥有自己的虚拟处理器。
Linux通过slab分配器分配task_struct
结构,从而达到对象复用和缓存着色。通过预先分配和重复使用task_struct
,可以避免动态分配和释放所带来的资源消耗。
1.1 进程状态如下:
1.2 进程上下文
一般程序在用户空间执行,当一个程序执行了系统调用或者触发了某个异常时,程序陷入内核空间,此时称内核代表进程执行并进入进程上下文。
CPU上下文及上下文切换:CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。
- CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。
- 程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
上下文切换:前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
1.3 进程创建
Linux进程创建通过fork()
和exec()
函数族来进行。首先fork()
通过拷贝当前进程创建一个子进程,父子进程的区别仅在于PID
、PPID
和某些资源和统计量。exec()
函数族负责读取可执行文件并将其加载到地址空间开始运行。
1.3.1 写时拷贝
写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个地址空间,而是让父子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程都有自己的拷贝,在此之前,以只读的方式共享。
1.3.2 fork()
Linux通过clone()
的系统调用实现fork()
,通过指定一系列参数标志来指明父子进程需要共享的资源。fork()
、vfork()
、__clone()
库函数都根据各自需要的参数标志去调用clone()
,后clone()
调用do_fork()
。
do_fork()
完成了创建中的大部分工作,该函数调用on_process()
函数,然后让进程开始运行。如果on_process()
函数成功返回,新创建的子进程被唤醒并让其投入运行。
2、线程
2.1 线程的实现
Linux把所有的线程都当作进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程,每个线程都拥有唯一隶属于自己的task_struct
,线程和其他的一些进程共享某些资源,如地址空间。
线程的创建和普通进程的创建类似,只不过在调用clone()
的时候需要传递一些参数标志来指明需要共享的资源。
2.2 内核线程
内核在需要执行一些后台操作时,这些任务可以通过内核线程来完成——独立运行在内核空间的标准进程。内核线程和普通线程之间的区别在于没有独立的地址空间,只在内核空间运行。同普通进程一样,可以被调度也可以被抢占。
3、进程终结
当进程终结时,内核对其所占用的资源进行释放并告知父进程。一般来说,进程的析构是自身引起的,发生在进行调用exit()
系统调用时,既可能显式调用,也可以隐式地从某个程序的主函数返回。当进程接收到它既不能处理也不能忽略的信号或者异常时,它还可能被动地终结。进程终结的大部分任务通过do_exit()
来完成。
调用do_exit()
后,线程已经不能运行,但系统依旧保留了它的进程描述符,这么做可以让系统有办法在子进程终结后仍能够获取它的信息。在父进程获取到已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct
才开始释放。
wait()
函数族通过调用wait4()
来实现。标准动作是挂起调用它的进程,直到其中一个子进程退出,此时函数会返回该子进程的PID
。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。
如果父进程在子进程之前退出,系统会给子进程在当前的线程组内找一个线程作为父亲,如果不行,就让init
做它们的父进程。在do_exit()
中会调用exit_notify()
,该函数会调用forget_original_parent()
,而后者会调用find_new_reaper()
来执行寻父过程。一旦系统为进程成功找到和设置了新的父进程,就不会有驻留僵死进程的风险了。init
进程会例行调用wait()
来检查其子进程,清除所有与其相关的僵死进程。