树莓派开始,玩转Linux21:进程的生与死

树莓派开始,玩转Linux21:进程的生与死

操作系统把计算机活动划分成进程。程序员编写的程序,也必须运行成进程,才能出现实际效果。既然进程在计算机活动中拥有如此关键的地位,那么我们理应更深入地了解进程。本章将介绍进程的创建和终结,以及与之相关的进程权限。

1.从init到进程树:

计算机开机时,Linux内核只创建了一个名为init的进程。在Linux运行期间,会有很多其他新进程,如Shell进程、音乐播放程序进程、邮件程序进程等。Linux内核不直接创建其他新进程,除了init进程之外的所有进程,都是通过fork机制创建的。

所谓的fork,就是从老进程中复制出一个新进程,英文中的"fork"是"分叉"的意思,如图所示。老进程就像一条带有分叉的小溪。老进程分出新进程后,老进程继续运行,成为新进程的父进程(Parent Process),新进程成为老进程的子进程(Child Process)。

在这里插入图片描述
查询当前Shell下的进程:
在这里插入图片描述
结果如下:
在这里插入图片描述
可以看到,第二个进程bash是第一个进程sudo的子进程,而第三个进程ps是第二个进程的子进程。

一个进程除了有一个PID之外,还会有一个PPID(Parent PID),即用来存储的父进程PID。子进程可以通过查询自己的PPID来了解自己的父进程。从任何一个进程出发,循着PPID不断向上追溯,总会发现源头是init进程,因此Linux的所有进程也构成了一个树状结构,这个树状结构以init进程为根。我们可以用pstree命令来显示树莓派的整个进程树。

在Linux中,fork无处不在。就拿Shell来说,当我们执行一个命令时,Shell进程就会fork一个子进程,用于执行命令对应的程序。在编写应用程序的过程中,也会用到fork机制,从而让一个新的进程来执行子任务。

2.fork系统调用:

Linux的应用程序可以通过fork系统调用来创建新进程。该系统调用发生后,就有父与子两个进程,而且两者的进程空间完全相同。

这就创造了一个"我是谁"的问题,即其中的一个进程如何知道,自己是父进程,还是子进程。

Linux内核已经考虑到了这一点,并通过fork调用的返回值解决了问题。由于fork之后有两个进程,fork系统调用会返回两次。一次返回到父进程,把子进程的PID作为返回值交给父进程。如果fork不成功,那么fork调用就会返回一个负值给父进程。
另一次返回到子进程,用0作为返回值。通过检查fork调用的返回值是否为0,进程就可以知道自己是否是子进程。
一方面,父进程可以通过fork的返回值知道子进程的PID。另一方面,进程又可以通过自己的PPID来知道父进程是谁,这样,进程就能明白自己在进程树中的位置了。
我们来看一个fork的例子。
在这里插入图片描述
在这里插入图片描述
程序中的getpid函数用于获得当前进程的PID。当fork返回后,内存中有两个进程。通过if区分出父进程和子进程,父进程将打印:

在这里插入图片描述
子进程将打印:
在这里插入图片描述
可见,进程通过fork返回弄清了自己是子进程还是父进程,就能根据自己的情况执行不同的任务。进程在获知自己是子进程后,通常会通过exec系列函数中的一个来加载新的程序文件,从而与父进程执行不同的任务。
比如Shell执行ls命令,会先fork自己的进程,然后在子进程中运行/bin/ls这一程序文件。

3.资源的fork:

进程空间记录进程的数据和状态。当进程fork时,Linux需要在内存中分配新的进程空间给新进程。此外,进程空间中的内容记录了进程的状态和数据。因此,原有进程空间中的所有内容,如程序段、全局数据、栈和堆,都要复制到新的进程空间中。在下面的程序中,子进程和父进程在fork之后有相同的栈,自然也会有相同的变量var。

在这里插入图片描述
除了进程空间,fork还会复制进程描述符(Process Descriptor)。内核中保存了每个进程的相关信息,即进程描述符。描述符是进程在内核中的"驻联合国代表"。每一个进程都会在内存中有一个对应的进程描述符。之前提到的PID、PPID和信号,都保存在进程描述符中。在fork之后,系统出现了一个新的进程,内核就要增加对应该进程的描述符。

子进程描述符中的很多内容都是从父进程的描述符中复制过来的。

· 当前工作目录。
· 环境变量。
· 已打开文件的相关信息。
· 信号mask和disposition。

这些信息都是程序运行必需的信息。如果子进程和父进程继续执行同一个程序,那么上述附加信息的改变,会造成子进程运行的错误。比如,父进程打开了一个文件,那么fork出的子进程也可以正常打开文件。

父进程和子进程描述符有很多信息不同。

· PID、PPID。
· 进程运行时间的相关信息在子进程中重置为0。
· 父进程的文件锁在子进程中被清空。
· 父进程的未处理信号在子进程中被清空。

这些信息都是描述进程个体特征的信息。子进程中描述符如果复制上述信息会出问题。我们已经知道,子进程和父进程的PID、PPID必然不同。此外,如果子进程的运行时间不重置为0,那么子进程运行时间的统计就不正确。
可见,进程描述符是否复制某一块信息,最重要的原则是保证新进程的正确运行。

4.最小权限原则:

Linux有一个"最小权限"(Least Privilege)的原则,就是收缩进程所享有的权限,以防进程滥用特权。进程权限也是根据用户身份进行分配的。然而,进程的不同阶段可能需要不同的特权。比如运行到中间时,需要先以更高的权限读入某些配置文件,再进行低权限的处理操作。由于权限和用户身份挂钩,这意味着进程需要在不同身份之间变化。

用户启动进程会让这个进程有3个身份:真实身份、存储身份和有效身份。每个身份都包含一套UID和GID。其中,真实身份是用户登录使用的身份。存储身份如果设置,就是程序文件的拥有者。有效身份则是判断进程权限时使用的身份。

在进程的运行过程中,进程可以从真实身份和存储身份中选择一个,复制到有效身份。通过这种机制,进程就可以在运行过程中变换权限。如果操作所需权限同时超越了真实身份和存储身份的权限,那么无论如何变换身份进程都无权操作。此外,并不是所有的程序都需要设置存储身份。需要这么做的程序文件会把权限的执行位上的"x"改为"s"。这时,用户权限的这一位叫作设置UID位(Set UID Bit),而组权限的这一位叫作设置GID位(Set GID Bit)。

5.进程的终结:

进程总有终结的时候。进程可以自发终结,比如进程在main函数结尾调用return,或者在程序中的某个位置调用exit函数直接退出。我们在信号中也看到,进程可以根据信号终结。此外,当进程出现致命错误时,比如当进程出现栈溢出错误时,内核也会主动终结进程。进程终结时,会有一个退出码。这个退出码可以是return或exit返回的,也可以是内核强制终结进程时设置的。当程序正常退出时,程序的退出码为0。如果运行过程中有错误或异常状况,那么退出码会是大于0的整数。退出码可以代表进程退出的原因。

当某个进程终结时,父进程会获得通知,进程空间随即被清空,然而,进程附加信息会保留在内核空间中。也就是说,即使一个进程终结了,它还是会在内核中留下痕迹。删除进程对应内核信息的重任,就落在父进程身上。

按照Linux的惯例,父进程有义务对子进程使用wait系统调用。在调用wait之后,父进程暂停,等待子进程终结。子进程终结后,父进程能从内核中取出子进程的退出信息,并清空这个子进程的进程描述符。在完成了上述工作之后,父进程将恢复运行。
下面是一段wait程序的示例:
在这里插入图片描述
在这里插入图片描述
如果父进程早于子进程终结,子进程就会和进程树失联,成为一个孤儿进程(Orphand Process)。孤儿进程会过继给init进程,因此进程init是所有孤儿进程的父进程。而对孤儿进程调用wait的责任,也就转交给了init进程。

当然,wait系统调用只是约定俗成的责任。Linux对此没有强制规定,一个程序也完全可以不对子进程调用wait,但这种程序会导致子进程的退出信息滞留在内核中。在这样的情况下,子进程成为僵尸进程(Zombie Process)。当大量僵尸进程积累时,内核空间会被挤占,优秀的程序员应该杜绝这种情况的发生。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值