Linux的进程, since 2020-11-01
进程Process
进程和程序的关系
程序规定了活动的动作,但应用程序不等于进程(process)。进程是程序的一个具体实现,只有运行程序,才能产生一个进程。程序与进程,类似于食谱和做菜的关系。进程是执行程序的过程。在Linux中用ps来查询正在运行的进程。
$ps -eo pid,cmd #-e表示列出全部进程,-eo pid,cmd表示需要的信息
上面指令的返回结果中,有的CMD名字用[]中括号包围起来的,是内核的一部分功能,被打扮成进程的样子方便操作系统管理。PID为1的进程一定是由/sbin/init程序运行而形成 (在MacOS中为/sbin/launchd)。
操作系统的一个重要功能就是管理进程,为进程提供必要的计算机资源,如内存空间,管理进程的相关信息。
进程的生成: fork机制
当Linux启动,init是系统内核创建的第一个进程也是唯一一个进程,它会一直存在直到关闭计算机。其他的进程,如音乐播放器Shell等等除了init外的所有进程,都是通过fork机制创建。fork为“分叉”,从老进程中复制出一个新进程。老进程分出新进程后,老金城继续运行,称为新进程的父进程(parent process),新进程称为老进程的子进程(child process)。在Shell中,进程ID称为PID,父进程ID称为PPID。用下面指令查询当亲Shell下的进程和其父进程。
$ps -o pid,ppid,cmd
任何进程,沿着PPID循迹而上,都会发现源头是init,查询结果显示,绝大多数进程的PPID都是1。Linux的所有进程构成了一个树状结构,该树以init为根,用pstree显示树莓派的整个进程树。
fork系统调用
通过fork系统调用生成新的进程,调用发生后就有父与子两个进程,而两者的进程空间完全相同。
系统如何知道自己是父与子中的哪一个?
fork之后有两个进程,fork系统调用会返回两次。
一次返回到父进程,把子进程的PID作为返回值交给父进程。如果fork不成功,那么fork调用就会返回一个负值给父进程。
另一次返回到子进程,用0作为返回值。检察fork调用的返回值,如果是0,进程就知道自己是子进程。父进程通过fork返回值知道子进程的PID,进程通过PPID知道自己的父进程,进程们因此知道自己在进程树中的位置。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void){
pid_t pid;
pid = fork();
if (pid < 0) {
printf('fork error\n');
exit(1);
} else if (pid == 0) {
/*子进程*/
printf('child: %d\n',getpid()); //getpid用于获取当前进程的PID
sleep(1);
} else {
/*父进程*/
printf('parent: %d\n', getpid());
sleep(2);
}
}
进程通过fork返回弄清了自己是子或父进程,就能根据情况执行不同的任务。获知自己是子进程后,可通过exec系统函数中的一个来加载新的程序文件,从而与父进程执行不同的任务。比如Shell执行ls指令,会先fork自己的进程,然后在子进程中运行/bin/ls这个程序文件。
资源fork
进程空间记录进程的数据和状态。当进程fork时,Linux需要在内存中分配新的进程间给新进程。进程空间还记录了进程的转台和数据。原有进程空间的所有内容,如程序段、全局数据、栈和堆,都要复制到新的进程空间中。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
pid_t pid;
int var = 1024;
pid = fork();
if (pid <0) {
printf('fork error\n');
exit(1);
} else if (pid == 0) {
/*子进程*/
printf('child: %d\n', var);
} else {
/*父进程*/
sleep(1);
printf('parent: %d\n', var);
}
return 0;
}
除了空间,fork还会复制进程描述符(process descriptor, pd)。内核中保存了每个进程的相关信息,即pd。每个进程都在内核中有一个对应的pd。PID、PPID、信号,都在pd中。
fork之后系统出现了新的进程,内核要增加对应进程的pd。child从parent继承的内容(即相同信息)包括:
- 当前工作目录 pwd
- 环境变量 env var
- 已经打开的文件的相关信息
- 信号mask和disposition
parent和child有的信息不同:
- PID/PPID
- 进程运行的相关信息在子进程中重置为0
- parent的文件锁在child中清空
- parent的未处理信号在child中被清空
这些信息都是描述进程个体特征的信息。
最小权限原则 least privilege
Linux中的该原则,收缩进程所享有的权限,以防进程滥用特权。进程权限也是根据用户身份进行分配。进程的不同阶段可能需要不同的特权。进程与身份挂钩,意味着进程需要在不同身份之间变化。
三个身份:真实身份、存储身份和有效身份。每个身份含有UID(user?)和GID(group?)。真实身份是用户登录使用的身份;存储身份如果设置,是程序文件的拥有者;有效身份是判断进程权限时使用的身份。
进程运行中,可从真实身份和存储身份中选择一个,复制到有效身份。并不是所有的程序都需要设置存储身份。需要这么做的程序文件会把权限的执行位上的x改为s。这时,用户权限的这一位叫做设置UID位(set UID bit),而组权限的这一位叫做设置GID位(set GID bit)。
进程终结
进程终结的情况
- main函数结尾调用return
- 程序中的某个位置调用exit函数退出
- 根据信号终结
- 进程出现致命错误,如进程出现栈溢出
进程终结时有一个退出码,正常退出时,退出码为0。有错误或者异常退出,退出码是大于0的整数。根据退出码可以得知进程退出的原因。
进程终结时,父进程会获得通知,进程空间随机被清空,而进程附加信息会保留在内核空间中。一个进程终结了,它会在内核中留下痕迹,删除进程对应内核信息的任务,由父进程完成。
按Linux惯例,父进程有义务对子进程使用wait系统调用。调用wait之后,父进程暂停,等待子进程终结。子进程终结后,父进程从内核中取出子进程的退出信息,并清除子进程的进程描述符pd。完成上述工作,父进程恢复运行。下面是wait程序的demo。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) //无法创建子进程
{ printf('error'); }
else if (pid == 0) //子进程
{ //do some calculation
sum = 0
for (int i = 0; i < 10000000; i++) {
sum += i;
}
exit(0);
}
else //父进程
{ int status;
printf('child PID %d...\n', pid);
//等待子进程结束
do {
waitpid(pid, &status, WUNTRACED);
} while (!WUNTRACED(status) && !WIFSIGNALED(status));
//status是子进程的返回值
printf('child return value %d\n', status);
}
}
如果父进程早于子进程终结,child就会和进程树失联,成为孤儿进程(orphand process),此进程会称为init的子进程,因此init是所有孤儿进程的parent,完成wait调用。但wait系统调用只是约定俗称的责任,不是Linux强制,一个程序完全可以不对子进程调用wait,但这会导致子进程的退出信息滞留在内核中,此时子进程成为僵尸进程(zombie process),僵尸进程累计,内核空间会被挤占,应避免发生。
Reference
1 Vamei,周昕梓著,树莓派开始玩转Linux,中国工信出版集团,电子工业出版社