Linux内核 之 进程

Linux内核主要包括:进程创建及调度,线程创建及调度,内核数据结构及系统调用,中断,内存管理,文件系统

在研究 Linux 实现之前,首先要对进程、进程组、会话,线程有个整体的了解:一个会话包含多个进程组,一个进程组包含多个进程,一个进程包含多个线程。

进程组和会话是为了支持 shell 作业控制而引入的概念。能够被函数修改。当有新的用户登录 Linux 时,登录进程会为这个用户创建一个会话。用户的登录 shell 就是会话的首进程。会话的首进程 ID 会作为整个会话的 ID。会话是一个或多个进程组的集合,包括了登录用户的所有活动。

进程组是一组相关进程的集合,会话是一组相关进程组的集合。

一个进程会有如下 ID:

1)PID:进程的唯一标识。如果一个进程含有多个线程,所有线程调用 getpid 函数会返回相同的值。

2)PGID:进程组 ID。每个进程都会有进程组 ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组 ID。

3)SID:会话 ID。每个进程也都有会话 ID。默认情况下,新创建的进程会继承父进程的会话 ID。

4)PPID:是程序的父进程号。

尽管在UNIX中,进程与线程是有联系但不同的两个东西,但在Linux中,线程只是一种特殊的进程。多个线程之间可以共享内存空间和IO接口。所以,进程是Linux程序的唯一的实现方式。

 

  • Linux中没有真正的线程

  • Linux中没有的线程Thread是由进程来模拟实现的所以称作:轻量级进程

  • 进程是「资源管理」的最小单元,线程是「资源调度」的最小单元(这里不考虑协程)

"进程——资源分配的最小单位,线程——程序执行的最小单位"

首先从OS设计原理上阐明三种线程:内核线程、轻量级进程、用户线程

内核线程就是内核的分身,一个分身可以处理一件特定事情。

轻量级线程(LWP)是一种由内核支持的用户线程。它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有LWP。

LWP虽然本质上属于用户线程,但LWP线程库是建立在内核之上的,LWP的许多操作都要进行系统调用,因此效率不高。而这里的用户线程指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助。因此这种线程的操作是极其快速的且低消耗的。

 

当计算机开机的时候,内核(kernel)只建立了一个init进程。Linux内核并不提供直接建立新进程的系统调用。剩下的所有进程都是init进程通过fork机制建立的。进程存活于内存中。每个进程都在内存中分配有属于自己的一片空间 (address space)。当进程fork的时候,Linux在内存中开辟出一片新的内存空间给新的进程,并将老的进程空间中的内容复制到新的空间中,此后两个进程同时运行。

老进程成为新进程的父进程(parent process),而相应的,新进程就是老的进程的子进程(child process)。一个进程除了有一个PID之外,还会有一个PPID(parent PID)来存储的父进程PID。如果我们循着PPID不断向上追溯的话,总会发现其源头是init进程。所以说,所有的进程也构成一个以init为根的树状结构。实际上,子进程总可以查询自己的PPID来知道自己的父进程是谁,这样,一对父进程和子进程就可以随时查询对方。

通常在调用fork函数之后,程序会设计一个if选择结构。当PID等于0时,说明该进程为子进程,那么让它执行某些指令,比如说使用exec库函数(library function)读取另一个程序文件,并在当前的进程空间执行 (这实际上是我们使用fork的一大目的: 为某一程序创建进程);而当PID为一个正整数时,说明为父进程,则执行另外一些指令。由此,就可以在子进程建立之后,让它执行与父进程不同的功能。

fork 函数会复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的 PPID 被设置成了原进程的 PID,信号位图被清除(也就是原进程设置的信号处理的函数不再对新进程起作用)。子进程的代码与父进程完全相同,同时它还会复制父进程的教据(堆数据、栈数据和靜态数据)。数据的复制采用的是所谓的写时复制(copy on writte),即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加 1,不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加 1。

当子进程终结时,它会通知父进程,并清空自己所占据的内存,并在内核里留下自己的退出信息(exit code,如果顺利运行,为0;如果有错误或异常状况,为>0的整数)。在这个信息里,会解释该进程为什么退出。父进程在得知子进程终结时,有责任对该子进程使用wait系统调用(等运行到wait,会等待直到子进程结束,会一直等待,是等待函数)。这个wait函数能从内核中取出子进程的退出信息(通常就是用wait来返回退出码),并清空该信息在内核中所占据的空间。但是,如果父进程早于子进程终结,子进程就会成为一个孤儿(orphand)进程。孤儿进程会被过继给init进程(就是直接让init继承了),init进程也就成了该进程的父进程。init进程负责该子进程终结时(往往是触发)调用wait函数。init 进程会周期性地去调用 wait 系统调用来清除它的僵尸孩子(注意说的没错,并不是孤儿进程)。

一个进程在调用了exit命令之后结束了自己的生命时候,它并没有真的被彻底销毁。其实不然,它只是变成了我们称之为“僵尸进程”状态。僵尸状态是每一个进程必须要经过的过程(除init进程之外)。它仅仅是在进程列表中保留一个位置,在这个位置中记载了该进程的状态。而它也只是静静的等待着其他进程(父进程或者init进程)为他收尸。又假如父进程是个无限循环的进程,那么子进程就会一直保持僵尸状态。这就能解释为什么系统运行久了,会出现大量的僵尸状态的进程。出现僵尸状态进程的数量少,是没有任何问题的,一旦出现巨量情况,就会导致PID用完,而给新的进程分配PID,当然也就会创建新进程失败。

孤儿进程是还在运行中的进程,而其父进程已经死掉了;而僵尸进程是已经死掉的进程,它的父进程并没有死掉,孤儿进程可以被托管给init,因而没有什么事情,而僵尸进程因为父进程托管,而父进程并不管它(因为太忙),这就造成过多僵尸进程无法被释放,pid被占用的情况。

僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们要消灭系统中大量的僵死进程时,要做的就是把产生大量僵死进程的那个父进程杀死。杀死之后,产生的僵死进程就变成了孤儿僵尸进程,这些孤儿僵尸进程会被 init 进程接管。

那么在编程时,如果能避免系统中大量产生僵尸进程呢?根据上面描述的,子进程在终止时会向父进程发 SIGCHLD 信号,Linux 默认是忽略该信号的,我们可以显示安装该信号,在信号处理函数中调用 wait 等函数来为其收尸,这样就能避免僵尸进程长期存在于系统中了。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值