Linux应用——进程基础
谁来调用 main 函数
在运行 main 函数之前,会有一段引导代码,最终由这段代码调用 main 函数,这段引导代码不需要自己编写,而是在编译、链接中由链接器将这段程序链接到应用程序中,构成最终的可执行文件,加载器会将可执行文件加载到内存中
进程的终止
正常终止
- 在 main 函数中通过 return 返回,终止进程
- 调用库函数 exit 终止进程
- 调用系统调用_exit/_Exit
异常终止
- 调用 abort 函数终止进程
- 被信号终止
终止进程:exit()和_exit()
exit()和_exit()用法
void _exit(int status)
:终止进程的运行,参数 status 表示进程终止时的状态,通常 0 表示正常终止,非零值表示发证了错误,如 open 打开文件失败(不是 abort 所表示的异常)void exit(int status)
:参数 status 含义同上
exit()和_exit()区别
exit()
是库函数,_exit()
是一个系统调用,他们所需要包含的头文件不同- 这两个函数的终止目的相同,都是终止进程,但在终止过程前需要做的处理不一样
exit()在终止进程的时候会调用终止处理函数:int atexit(void (*function)(void));,可以调用多个 atexit,调用顺序和注册顺序相反
标准输出默认是行缓存,检测到"\n"后才会把该行输出,_exit()不会刷新 IO 缓存,因此没有"\n"的情况时该行不会输出
- 不会刷新 stdio 缓冲的情况
- _exit()/_Exit()
- 被信号终止
exit()和 return 的区别
- exit()为库函数,return 为 C 语言的语句
- exit()函数最终会进入到内核,把控制权交给内核,最终由内核去终止进程;return 并不会进入内核,只是一个函数的返回,返回到它的上层调用,最终由上层调用终止进程
- return 和 exit 同样会调用终止处理函数、刷新 IO 缓存
exit()和 abort 区别
- exit 函数用于正常终止进程(执行一些清理工作),abort 用于异常终止进程(不会执行清理工作,会直接终止进程),abort 本质上是直接执行 SIGABRT 信号的系统默认处理操作
进程的环境变量
环境变量的概念
- 环境变量是指在进程运行环境中定义一些变量,类似于进程的全局变量,可以在程序的任何地方获取,只需声明即可。但与全局变量不同的是,这些环境变量可以被其他子进程所继承,也就是具有继承性
- 环境变量的本质还是变量,不过这些变量没有类型,都是以字符串的形式存储在一个字符串数组当中,称为环境表(以 NULL 结尾),数组中的每个环境变量都是以
name = value
这种形式定义的,name 表示变量名称,value 表示变量值
环境变量相关命令
env
:使用命令查看环境变量echo $name
:查看环境变量export name=value
:自定义/修改环境变量(注意等号前后不要有空格)unset name
:删除环境变量
常见的环境变量
PATH
:用于指定可执行程序的搜索路径HOME
:当前用户的家目录LOGNAME
:指定当前登录的用户HOSTNAME
:指定主机名SHELL
:指定当前 shell 解析器PWD
:指定进程的当前工作目录
环境变量的组织形式
在应用程序中获取环境变量
在每个应用程序中都有一组环境变量,是在进程创建中从父进程中继承过来的
- environ 变量获取:全局变量,可以直接在程序中使用,只需要申明就好,environ 实际上是一个指针,指向环境表
extern char **environ; // 申明一下,即可使用environ[i]
- 通过 main 函数获取(尽量不要使用这种方式,有的系统可能不支持)
int main(int argc, char *argv[], char *env[]); // 第三个参数为进程的环境表
- 通过 getenv 获取指定的环境变量(库函数)
#include <stdlib.h>
char *getenv(const char *name); // 如果存放该环境变量,则返回该环境变量的值对应字符串的指针;如果不存在该环境变量,则返回NULL
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
// 测试
int main(int argc, char *argv[]){
char *get_str;
get_str = getenv(argv[1]);
if(get_str == NULL){
printf("error!\n");
exit(1);
}
printf("%s\n", get_str);
exit(0);
}
使用 getenv()需要注意,不应该去修改其返回的字符串,修改该字符串意味着修改了环境变量对应的值,Linux 提供了相应的修改函数,如果需要修改环境变量的值应该使用这些函数,不应直接改动该字符串。
添加/修改/删除环境变量
putenv
:添加/修改环境变量(有对应的 name 则修改,没有则添加)
#include <stdlib.h>
int putenv(char *string); // string是一个字符串指针,指向name=value形式的字符串;成功返回 0,失败将返回非0值,并设置 errno
该函数调用成功之后,参数 string 所指向的字符串就成为了进程环境变量的一部分了,换言之,putenv()函数将设定 environ 变量中的某个元素指向该 string 字符串,而不是指向它的复制副本,这里需要注意!因此,不能随意修改参数 string 所指向的内容,这将影响进程的环境变量,参数 string 不应为自动变量(即在栈中分配的字符数组),因为自动变量的生命周期是函数内部,出了函数之后就不存在了(可以使用 malloc 分配堆内存,或者直接使用全局变量)
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
// 测试
int main(int argc, char *argv[]){
char *get_str;
if(putenv(argv[1])){
printf("error!\n");
exit(-1);
}
for(int i = 0; environ[i] != NULL; i++){
printf("%s\n", environ[i]);
}
return 0;
}
上述代码 putenv 在本进程范围内修改了环境变量,进程结束后,原来的环境变量不变
setenv
:添加/修改环境变量(推荐使用这个)(可替代 putenv 函数)
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite); // name为环境变量的名称,value为环境变量的值
// overwrite:若name标识的环境变量已经存在,在参数overwrite为0的情况下,setenv()函数将不改变现有环境变量的值,也就是说本次调用没有产生任何影响;如果参数overwrite的值为非0,若参数name标识的环境变量已经存在,则覆盖,不存在则表示添加新的环境变量。
- setenv 和 putenv 的区别:
- setenv 会将用户传入的 name=value 字符串拷贝到自己的缓冲区中,而 putenv 不会
- setenv()可通过参数 overwrite 控制是否需要修改现有变量的值而仅以添加变量为目的,显然 putenv()并不能进行控制
name=value ./test
:在进程执行时添加环境变量(可同时添加多个环境变量,用空格隔开)unsetenv
:从环境表中移除参数 name 标识的环境变量
#include <stdlib.h>
int unsetenv(const char *name);
清空环境变量
- 将 environ 设置为 NULL
- 通过 clearenv 来清空环境变量
#include <stdlib.h>
int clearenv(void);
创建子进程
所有的子进程都是由父进程创建出来的
- 比如在终端执行./test,这个进程是由 shell 进程(bash、sh 等 shell 解析器)创建出来的
- 最原始的进程为 init 进程,它的 PID 为 1,由它创建出其他进程
getpid()获取当前进程的 PID,getppid()获取当前进程父进程的 PID,命令行中通过
ps-aux
/pstree -T
命令查看 PID
父子进程间的文件共享
- 文件共享:多个进程、多个线程对同一文件进行读写操作
- 子进程会拷贝父进程打开的所有文件描述符(fd)
- 验证父子进程间的文件共享是按照接续写(使用这个)还是分别写:
接续写:两个文件描述符指向同一个文件表,使用同一个读写指针
分别写:可能会出现数据覆盖的情况
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 测试
int main(void){
int fd;
int pid;
fd = open("./test.txt", O_WRONLY | O_TRUNC);
if(fd == -1){
printf("error");
return 1;
}
pid = fork();
if(pid > 0){
printf("parent %d %d\n", pid, getppid());
write(fd, "123456", 6);
close(fd);
}else if(pid == 0){
printf("child %d %d\n", getpid(), getppid());
write(fd, "Hello World", 11);
close(fd);
}else{
printf("build error\n");
exit(-1);
}
exit(0);
}
父子进程间的竞争关系
fork 之后不知道是父进程先返回还是子进程先返回,由测试结果来看,绝大部分情况下是父进程先返回
父进程监视子进程
-
父进程需要知道子进程的状态改变:
- 子进程终止
- 子进程因为收到停止信号而停止运行(SIGSTOP、SIGTSTP)
- 子进程在停止状态下因为收到恢复信号而恢复运行(SIGCONT)
-
以上也是 SIGCHLD 信号的三种触发情况,当子进程发生状态改变时,内核会向父进程发送这个 SIGCHLD 信号
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
wait 函数
- wait 函数为系统调用,可以等待进程的任一子进程终止,同时获取子进程的终止信息(监视子进程第一种状态的改变),作用:
- 监视子进程什么时候被终止,以及获取子进程终止时的状态信息
- 回收子进程的一些资源(俗称“收尸”)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus); // wstatus用于存放子进程终止时的状态信息,可以设置为NULL,表示不接收子进程终止时的状态信息
// 返回值:若成功返回终止的子进程对应的进程号,失败则返回-1
- 进程调用 wait()函数的情况:
- 如果该进程没有子进程(即没有需要等待的子进程),那么 wait()将返回-1,并且将 errno 设置为 ECHILD
- 如果该进程所有子进程都还在运行,则 wait()会一直阻塞等待,直到某个子进程终止
- 如果调用 wait()之前该进程已经有一个或多个子进程终止了,那么调用 wait()不会阻塞,会回收子进程的一些资源,注意一次 wait 调用只能为一个已经终止的子进程“收尸”
- status 为
NULL
或者(int *)0
时,返回该退出的子进程的 PID 号 - 如果父进程关注子进程的退出时状态,可以使用如下方式,
status
将保存子进程结束时的状态信息(子进程退出时exit
里的参数会被保存到status
中)
- status 为
int status;
wait(&status);
- 可以通过以下宏来检查 status 参数:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
int main(void){
int pid = fork();
int ret;
int status;
if(pid > 0){
printf("parent %d %d\n", pid, getppid());
ret = wait(&status);
printf("wait return %d %d\n", ret, WEXITSTATUS(status));
exit(0);
}else if(pid == 0){
printf("child %d %d\n", getpid(), getppid());
exit(3);
}else{
printf("build error\n");
exit(-1);
}
exit(0);
}
- 使用 wait()的限制:
- 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
- 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
- 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了(没法监视后两种状态改变)
waitpid 函数
waitpid 函数没有 wait 函数存在的限制
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options); // wstatus的含义同wait里的
// 返回值:与wait基本相同,但参数options包含了WNOHANG标志时,返回值可能会出现0
-
参数 pid 表示需要等待的某个具体子进程,取值如下:
pid > 0
:等待进程号为 pid 的子进程pid = 0
:等待该父进程的所有子进程pid < -1
:等待进程组标识符与 pid 绝对值相等的所有子进程(特殊情况可能为负数)pid = -1
:等待任意子进程
-
参数 options 是一个位掩码,设置为 0 时功能和 wait 相同(pid 为-1 时):
WNOHANG
:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变(可以实现轮询 poll 来监视子进程的状态)WUNTRACED
:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息WCONTINUED
:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息
异步方式监视子进程
-
可以为 SIGCHLD 信号(子进程退出时发给父进程的信号)绑定一个信号处理函数(为父进程绑定),然后在信号处理函数中调用 wait/waitpid 函数回收子进程(针对第一种状态,其他两种状态可以进行相应处理)
-
这样可以使得父进程做自己的事情(异步),不用阻塞或者轮询等待子进程结束(也可以通过多线程来实现)
-
使用这一方法的注意事项:
- 当调用信号处理函数时,会暂时将引发调用的信号添加到进程的信号掩码中(除非 sigaction()指定了 SA_NODEFER 标志),这样一来,当 SIGCHLD 信号处理函数正在为一个终止的子进程“收尸”时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号,结果是,父进程的 SIGCHLD 信号处理函数每次只调用一次 wait(),那么就会导致有些僵尸进程成为“漏网之鱼”
- 解决方案就是:在 SIGCHLD 信号处理函数中循环以非阻塞方式来调用 waitpid(),直至再无其它终止的子进程需要处理为止,所以,通常 SIGCHLD 信号处理函数内部代码为:
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
上述代码一直循环下去,直至 waitpid()返回 0,表明再无僵尸进程存在;或者返回-1,表明有错误发生。
僵尸进程和孤儿进程
-
孤儿进程:父进程先于子进程结束,在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程,换言之,某一子进程的父进程结束后,init 进程变成了孤儿进程的“养父”;这是判定某一子进程的“生父”是否还“在世”的方法之一
-
僵尸进程:子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。
- 当父进程调用 wait()(或waitpid、waitid等)为子进程“收尸”后,僵尸进程就会被内核彻底删除
- 如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(),故而从系统中移除僵尸进程
- 如果父进程创建了某一子进程,子进程已经结束,而父进程还在正常运行,但父进程并未调用 wait()回收子进程,此时子进程变成一个僵尸进程。首先来说,这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是SIGKILL信号也不行,那么这种情况下,只能杀死僵尸进程的父进程或者等待其父进程终止,init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉)
- 所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait()将其回收,避免僵尸进程
执行新程序
- 子进程和父进程运行的不是同一个程序,比如test进程通过fork函数创建子进程后,这个子进程也运行test这个程序,当这个子进程启动后,通过调用库函数或者系统调用用一个新的程序去替换test程序,然后从main函数开始执行这个新程序
execve函数
- execve为系统调用,可以将新程序加载到某一进程的内存空间,通过调用 execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的main()函数开始执行
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
- 参数及返回值含义:
- pathname:指向新程序的路径名(绝对路径/相对路径),对应 main(int argc, char *argv[])的 argv[0]
- argv[]:传递给新程序的命令行参数(字符串数组,以 NULL 结束),对应 main 函数的 argv 参数
- envp:指定了新程序的环境变量列表,对应新程序的 environ 数组,以 NULL 结束
- 返回值:调用成功不会返回(执行新程序去了),失败返回-1,并设置 errno
注意 pathname 可以是路径:./test,也可以是可执行文件名称:test(一切接文件)(在同一个目录下)
exec 族库函数
- exec 族库函数基于 execve 系统调用来实现
#include <unistd.h>
extern char **environ;
// execl("/bin/ls", "ls", "-a", "-l", NULL);
int execl(const char *pathname, const char *arg, .../* (char *) NULL */);
// execlp("ls", "ls", "-a", "-l", NULL);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
// execle("/bin/ls", "ls", "-a", "-l", NULL, environ);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
// execv("/bin/ls", argv[1]);
int execv(const char *pathname, char *const argv[]);
// execvp("ls", argv[1]);
int execvp(const char *file, char *const argv[]);
// execvp("ls", argv[1], environ);
int execvpe(const char *file, char *const argv[], char *const envp[]);
- 参数含义
- pathname 同 execve,指向新程序的路径名,file 参数指向新程序文件名,它会去进程的 PATH 环境变量所定义的路径寻找这个新程序(兼容绝对路径和相对路径)
- arg 参数将传递给新程序的参数列表依次排列,通过多个字符串来传递,以 NULL 结尾
- 默认情况下,新程序保留原来程序的环境表
system 函数
-
system 为库函数,可以很方便地在程序中执行任意 shell 命令
-
system 内部实现原理:system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能。首先 system()会调用 fork()创建一个子进程,然后子进程会调用 execl()加载一个shell 解释器程序(通常是/bin/sh程序),这是子进程就是一个 shell 进程了,这个 shell 子进程解析 command 参数对应的命令,并创建一个或多个子进程执行命令(命令时可执行程序,每执行一个命令,shell 子进程就需要创建一个进程然后加载命令/可执行程序),当命令执行完后 shell 子进程会终止运行,system()中的父进程会调用 waitpid()等待回收shell 子进程,直到 shell 子进程退出,父进程回收子进程后,system 函数返回。
-
system 每执行一个 shell 命令,system 函数至少要创建两个子进程:
- system 函数创建 shell 子进程
- shell 子进程根据命令创建它的子进程(一个或多个,根据命令而定)
#include <stdlib.h>
int system(const char *command); // system("ls -al")
- 参数及返回值含义:
- command:指向需要执行的 shell 命令,如"ls -al"
- 返回值:
- 当参数 command 为 NULL,如果 shell 可用则返回一个非 0 值,若不可用则返回 0;针对一些非 UNIX 系统,该系统上可能是没有 shell 的(bash/sh/csh),这样就会导致 shell 不可用
- 如果 command 不为 NULL,则:
- 如果 system 无法创建子进程(fork 失败)或无法获取子进程的终止状态(waitpid 返回-1),那么 system()返回-1
- 如果子进程不能执行 shell(execl 执行不成功),则 system()的返回值就是子进程通过调用_exit(127)终止了
- 如果所有的系统调用都成功,system()函数会返回执行 command 的 shell 进程的终止状态(执行最后一个命令的终止状态)
// 根据system函数的功能以及该函数在不同情况下的返回值所实现的一个简易的system函数
int system(const char *command){
if(command == NULL){ // 返回值1
if("当前系统中存在可用的shell解析器程序 bash/sh/csh")
return "非零值";
else
return 0;
}
pid_t pid = fork(); // 创建子进程,该子进程会变为shell子进程
switch(pid){
case -1: // 创建子进程失败,返回值2
return -1;
// 子进程
case 0:
excel("/bin/sh", "sh", "-c", command, NULL); // 加载shell解析器,如果成功不会返回
_exit(127); // 加载shell解析器失败,调用_exit(127),返回值3
// 父进程
default:
int status;
int ret;
ret = waitpid(pid, &status, NULL); // 等待回收子进程
if(ret == -1)
return -1; // 无法获取子进程的状态信息,返回值1
return status; // 返回子进程的状态信息
} // 如果所有系统调用都成功,那么system返回shell子进程的终止状态信息
// 即返回执行最后一个命令的终止信息,返回值4
}
- system()在使用上简单,但是是以牺牲效率为代价的
vfork 函数
fork 系统调用使用场景
- 父进程希望子进程复制自己,父子进程执行相同的程序,各自在自己的进程空间中运行
- 子进程执行一个新的程序,从该程序的 main 函数开始运行,调用 exec 函数
fork 函数的缺点
fork+exec 配合使用时,效率比较低
vfork 函数
vfork 为系统调用,也是用来创建一个进程,返回值也是一样的
fork 与 vfork 不同点
- 对于 fork 函数,fork 会为子进程创建一个新的地址空间(也就是进程空间),子进程几乎完全拷贝了父进程,包括数据段、代码段、堆、栈等;而对于 vfork 函数,子进程在终止或者成功调用 exec 函数之前,子进程与父进程共享地址空间,共享所有内存,包括数据段、堆栈等,所以在子进程在终止或成功调用 exec 函数前,不要去修改除 vfork 的返回值的 pid_t 类型的变量之外的任何变量(父进程的变量)、也不要调用任何其它函数(除 exit 和 exec 函数之外的任何其它函数),否则将会影响到父进程(vfork 函数的正确使用方法就是创建子进程后立马调用 exec 加载新程序,所以没有意义去调用其他函数或者修改变量)
注意:vfork 创建的子进程如果要终止应调用 exit,而不能调用 exit 或 return 返回,因为如果子进程调用 exit 或 return 终止,则会调用父进程绑定的终止处理函数以及刷新父进程的 stdio 缓冲,影响到父进程
- 对于 fork 函数,fork 调用之后,父、子进程的执行次序不确定;而对于 vfork 函数,vfork 函数会保证子进程先运行,父进程此时处于阻塞、挂起状态,在子进程终止或成功调用 exec 函数之后,父进程才会被调度运行
注意:如果子进程在终止或成功调用 exec 函数之前,依赖于父进程的进一步动作,将会导致死锁!
- vfork 函数在创建子进程时,不用拷贝父进程的数据段、代码段、堆栈等,所以 vfork 函数的效率要高于 fork 函数
目前的 fork 函数使用了写时复制技术,效率还算可以,所以尽量不要用 vfork,以免产生一些难以排查的问题
进程状态和进程间的关系
进程状态
-
进程状态有六种:
- R(TASK_RUNNING):运行状态或可执行状态(就绪态):正在运行的进程或者在进程队列中等待运行的进程都处于该状态,所以该状态实际上包含了运行态和就绪态这两个基本状态
- S(TASK_INTERRUPTIBLE):可中断睡眠状态(浅度睡眠):可中断睡眠状态也被称为浅度睡眠状态,处于这个状态的进程由于在等待某个事件(等待资源有效)而被系统挂起,譬如等待 IO 事件、主动调用 sleep 函数等。一旦资源有效时就会进入到就绪态,当然该状态下的进程也可被信号或中断唤醒(所以可中断的意思就是,即使未等到资源有效,也可被信号中断唤醒,譬如 sleep(5)休眠 5 秒钟,通常情况下 5 秒未到它会一直睡眠、阻塞,但在这种情况下,收到信号就会让它结束休眠、被唤 )
- D(TASK_UNINTERRUPTIBLE):不可中断睡眠状态(深度睡眠):不可中断睡眠状态也被称为深度睡眠状态,该状态下的进程也是在等待某个事件、等待资源有效,一旦资源有效就会进入到就绪态;与浅度睡眠状态的区别在于,深度睡眠状态的进程不能被信号或中断唤醒,只有当它所等待的资源有效时才会被唤醒(一般该状态下的进程正在跟硬件交互、交互过程不允许被其它进程中断)
- T(TASK_STOPPED):停止状态(暂停状态):当进程收到停止信号时(譬如 SIGSTOP、SIGTSTP 等停止信号),就会由运行状态进入到停止状态。当处于停止状态下,收到恢复信号(譬如 SIGCONT 信号)时就会进入到就绪态
- Z(TASK_ZOMBIE):**僵尸状态 **:表示进程已经终止了,但是并未被其父进程所回收,也就是进程已经终止,但并未彻底消亡。需要其父进程回收它的一些资源,归还系统,然后该进程才会从系统中彻底删除
- X(TASK_DEAD): 死亡状态:此状态非常短暂、ps 命令捕捉不到。处于此状态的进程即将被彻底销毁,可以认为就是僵尸进程被回收之后的一种状态
-
ps 命令查看到的进程状态信息中,除了第一个大写字母用于表示进程状态外,还有其他一些字符:
- s:表示当前进程是一个会话的首领进程
- l:表示当前进程包含了多个线程
- N:表示低优先级
- <:表示高优先级
- +:表示当前进程处于前台进程组中
进程间的关系
两个进程之间的关系主要包括:父子关系、进程组、会话
进程组
-
需要注意以下问题:
- 每个进程必定属于某一个进程组,并且只能在一个进程组中
- 每一个进程组都有一个组长进程(创建进程组的进程),组长进程的进程 ID (PID)就等于该进程组的进程组 ID(PGID)。
- 只要进程组中还存在至少一个进程,那么该进程组就存在,这与其组长进程是否终止无关(组长进程终止并不一定导致进程组终止)
- 一个进程组可以包含一个或多个进程,进程组的生命周期从创建开始,直到组内所有的进程终止或离开该进程组
- 默认情况下,新创建的进程会继承父进程的进程组 ID(PGID),子进程与父进程在同一个进程组中
-
获取/创建进程组
#include <sys/types.h>
#include <unistd.h>
pid_t getpgid(pid_t pid); // 参数pid为指定要获取哪个进程的进程组ID,如果参数为0表示调用者进程组的ID;如果调用成功,返回进程组ID,失败返回-1,并设置errno
int setpgid(pid_t pid, pid_t pgid); // 用法1:将参数pid指定的进程的进程组ID设置为参数pgid(要保证这两个进程组在同一个会话中);用法2:如果gpid所指定的进程组不存在,那么会创建一个新的进程组,由参数pid指定的进程作为这个进程组的组长(这种情况下要保证pid = pgid);特殊取值:如果参数pid等于0,则表示使用调用者的进程ID;如果参数gpid等于0,则表示第二个参数设置为等于第一个参数pid
pid_t getpgrp(void); // 返回值为调用者进程对应的进程组ID,等价于getpid(0) /* POSIX.1 version */
pid_t getpgrp(pid_t pid); /* BSD version *///(函数重载)
int setpgrp(void); //等价于setpgid(0, 0)=setpgid(getpgrp(), getpgrp()) /* System V version */
int setpgrp(pid_t pid, pid_t pgid); /* BSD version */
一个进程只能修改它自己或它的子进程所属的进程组,并且子进程在调用 exec 之后就不能再修改子进程所属的进程组了
会话
-
需要注意的问题:
- 每个进程组必定属于某个会话,并且只能在一个会话中
- 一个会话包含一个或多个进程组,最多只能有一个前台进程组(前台作业)(可以没有),其它的都是后台进程组(后台作业)
- 每个会话都有一个会话首领(首领进程),即创建会话的进程
- 同样每个会话也有 ID 标识,称为会话 ID(简称:SID),每个会话的 SID 就是会话首领进程的进程 ID(PID)。所以如果两个进程的 SID 相同,表示它们俩在同一个会话中。在应用程序中调用 getsid 函数获取进程的 SID
- 会话的生命周期从会话创建开始,直到会话中所有进程组生命周期结束,与会话首领进程是否终止无关
- 一个会话可以有控制终端、也可没有控制终端,每个会话最多只能连接一个控制终端。控制终端与会话中的所有进程相关联、绑定,控制、影响着会话中所有进程的一些行为特性,譬如控制终端产生的信号,将会发送给该会话中的进程(譬如 CTRL+C、CTRL+Z、CTRL+\ 产生的中断信号、停止信号、退出信号,将发送给前台进程组);譬如前台进程可以通过终端与用户进行交互、从终端读取用户输入的数据,进程产生的打印信息会通过终端显示出来;譬如当控制终端关闭的时候,会话中的所有进程也被终止
- 当我们在 Ubuntu 系统中打开一个终端,那么就创建了一个新的会话(shell 进程就是这个会话的首领进程,也就意味着该会话的 SID 等于 shell 进程的 PID),打开了多少个终端,其实就是创建了多少个会话
- 默认情况下, 新创建的进程会继承父进程的会话 ID,子进程与父进程在同一个会话中 (也可以说子进程继承了父进程的控制终端)
-
关于前台与后台的一些操作:
- 执行程序时,后面添加
&
使其在后台运行 - fg 命令可以将后台进程调至前台继续运行
- Ctrl+Z 可以将前台进程调至后台,并处于停止状态(暂停状态)
- 执行程序时,后面添加
注意前台进程组中的所有进程都是前台进程,所以终端产生的信号( CTRL+C、CTRL+Z、CTRL+\ )它们都会接收到
- 获取/创建会话
#include <sys/types.h>
#include <unistd.h>
pid_t getsid(pid_t pid); // 如果参数pid为0,则返回调用者进程的会话ID;如果参数pid不为0,则返回参数pid指定的进程对应的会话ID;如果失败的话返回-1,并设置errno
pid_t setsid(void); // 如果调用者进程不是进程组的组长进程(如果是组长则不能使用setsid),则创建一个**新会话**,调用者进程是新会话的首领进程,也会创建一个**新的进程组**(因为一个会话至少要存在一个进程组),调用者进程也是新进程组的组长进程,但是该会话**没有控制终端、脱离控制终端** (ps 命令可以查看进程的控制终端TTY)
// setsid的返回值:如果成功,则返回新会话的SID,如果失败返回-1,并设置errno
守护进程
什么是守护进程
守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:
Linux 系统中有很多系统服务,大多数服务都是通过守护进程来实现的,譬如系统日志服务进程 syslogd、web 服务器 httpd、邮件服务器 sendmail 和数据库服务器 mysqld 等。守护进程(Daemon)的名字通常以字母 d 结尾
编写守护进程
守护进程的重点在于脱离控制终端,但是除了这个关键点之外,还需要注意其它的一些问题,编写守护进程一般包括如下几个步骤:
父进程信号处理机制对子进程的影响
父进程绑定的信号处理函数对子进程的影响
fork 后子进程会继承父进程绑定的信号处理函数,如果调用 exec 加载新程序后,就不会再继承这个信号处理函数了
父进程的信号掩码对子进程的影响
fork 后子进程会继承父进程的信号掩码,执行 exec 后仍会继承这个信号掩码