进程
进程的概念
何为进程?
-
进程是一个应用程序的执行实例,也就是系统中正在运行的应用程序,程序一旦运行就是进程
-
进程是一个动态过程,它是程序的一次运行过程,而非静态文件
-
同一个程序可以被运行多次,从而产生多个不同的进程,可以把进程理解为程序的实例化对象
进程号(process ID)
-
Linux 系统下的每一个进程都有一个进程号(process lD,简称 PID),进程号是一个正数用于唯一标识系统中的某一个进程。
-
执行ps -aux它就会打印系统中正在运行的所有进程
-
可通过系统调用 getpid()来获取本进程的进程号
-
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); -
函数返回值为 pid_t 类型变量,便是对应的进程号
-
main函数由谁调用?
-
操作系统下的应用程序在运行 main()函数之前需要先执行一段引导代码,最终由这段引导代码去调用应用程序的 main()函数。我们在编写应用程序的时候,不用考虑引导代码的问题,在编译链接时,由链接器将引导代码链接到我们的应用程序当中,一起构成最终的可执行文件
-
加载器会将应用程序加载内存中去执行
进程如何终止
-
正常终止
-
main()函数中通过 return 语句返回来终止进程
-
应用程序中调用 exit()库函数终止进程
-
应用程序中用系统调用_exit()或_Exit()终止进程
-
-
异常终止
-
应用程序中调用 abort()函数终止进程
-
进程接收到一个信号,譬如 SIGKILL 信号
-
终止进程:exit()和_exit()
exit、_exit的用法
-
void _exit(int status);
-
系统调用,终止进程的运行
-
status:这个参数用来表示进程终止时的状态,通常0表示正常终止,非零值表示非正常终止(指发生了错误,譬如open打开文件失败,并非是abort所表示的异常)
-
-
void exit(int status);
-
库函数,终止进程的运行
-
status:这个参数用来表示进程终止时的状态,通常0表示正常终止,非零值表示非正常终止(指发生了错误,譬如open打开文件失败,并非是abort所表示的异常)
-
exit和_exit之间的区别
-
exit()是库所数,_exit()它是一个系统调用,他们所需要的包含的头文件不一样
-
这两个函数的最终目的相同,都是终止进程,但是在终止进程之前需要做一些处理这些处理工作这两个函数是不一样的
- 终止处理函数可以注册多个,调用顺序和注册顺序相反 - atexit()注册终止处理函数 - 标准输出默认采用的是行缓冲模式,也就是它只有检测到换行符反斜杠n的时候,才会把这句话的输出 - 有一些终止进程的情况是不会刷新stdio缓冲的 - _exit()或 _Exit() - 被信号终止的情况
exit和return之间的区别
-
exit()是一个库函数,return是C语言的语句
-
exit()函数最终会进入到内核,把控制权交给内核,最终由内核去终止进程。
-
执行return并不会进入到内核,它只是一个main函数返回,返回到它的上层调用把控制权交给他的上层调用,最终由上层调用终止进程。
-
使用return终止进程也会调用终止处理函数,并且会刷新IO缓冲
exit和abort之间的区别
-
exit函数用于正常终止进程。abort函数用于异常终止进程
-
正常终止在终止进程之前,会执行一些处理工作。异常终止并不会执行这些清理工作。它是直接终止进程。执行SIGABRT信号的系统默认操作
-
abort既不会调用这个终止处理函数,也不会刷新IO缓冲
进程的环境变量
环境变量的概念
-
每个进程都关联一组环境变量,这些变量存储在名为环境列表的字符串数组中。每个环境变量都是一个“名称=值”格式的字符串,构成了一组“名称-值”对的集合。
-
使用 env 命令查看环境变量
-
echo 查看指定$环境变量
-
export 导出、添加、修改一个新的环境变量
-
unset、export -n 删除环境变量
常见的环境变量
-
PATH:用于指定可执行程序的搜索路径
-
HOME:用于指定当前用户的家目录
-
LOGNAME:用于指定当前登录的用户
-
HOSTNAME:用于指定主机名
-
SHELL:用于指定当前的 shell 解析器
-
PWD:用于指定进程的当前工作目录
环境变量的组织形式
应用程序中获取环境变量
-
每一个应用程序都有一组环境变量,进程在创建的时候,它的环境变量从的其父进程中继承过来
-
通过 environ 变量获取
-
这个变量是一个全局变量,我们可以在程序中直接使用,只需要申明即可
-
environ 变量其实是一个指针,它指向一个字符串数组,这个字符串数组就是进程的环境表
- extern char **environ; // 申明外部全局变量 environ
-
-
通过 main 函数的参数获取
-
int main(int argc, char *argv[])
-
int main(int argc, char *argv[], char *env[])
- 第三个参数 env 其实就是进程的环境表
-
-
通过 getenv 获取指定环境变量
-
库函数
-
include <stdlib.h>
char *getenv(const char *name);-
name:指定获取的环境变量名称
-
返回值:如果存放该环境变量,则返回该环境变量的值对应字符串的指针;如果不存在该环境变量,则返回 NULL
-
-
使用 getenv() 函数获取环境变量时,应避免修改返回的字符串,因为这会改变环境变量的值。正确的做法是使用 Linux 提供的特定函数来修改环境变量的值,而不是直接修改 getenv() 返回的字符串。
-
添加/修改/删除环境变量
-
添加/修改环境变量
-
putenv()函数
-
向进程的环境变量数组中添加一个新的环境变量,或者修改一个已经存在的环境变量对应的值
-
#include <stdlib.h>
int putenv(char *string);-
string:参数 string 是一个字符串指针,指向 name=value 形式的字符串
-
返回值:成功返回 0;失败将返回非 0 值,并设置 errno
-
-
putenv() 函数将进程环境变量列表 environ 中的一个元素指向传入的参数 string 所指向的字符串,而不是其副本。因此,修改 string 会影响环境变量。为避免问题,string 不应是栈上分配的自动变量,因为自动变量的生命周期是函数内部,出了函数之后便不再有效了!譬如说可以使用 malloc 分配堆内存,或者直接使用全局变量
-
-
setenv()函数
-
setenv()函数可以替代 putenv()函数。setenv()函数为形如 name=value 的字符串分配一块内存缓冲区,并将参数 name 和参数 value 所指向的字符串复制到此缓冲区中,以此来创建一个新的环境变量
-
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);-
name:需要添加或修改的环境变量名称
-
value:环境变量的值
-
overwrite:如果参数overwrite为0,且指定的环境变量已存在,则该函数不会改变现有环境变量的值,即此次调用无效。如果overwrite为非0,那么如果指定的环境变量已存在,则其值将被更新;如果不存在,则会添加一个新的环境变量
-
返回值:成功返回 0;失败将返回-1,并设置 errno
-
-
setenv()与 putenv()函数有两个区别
-
putenv,函数内部不会备份用户传入的字符串参数,而是直接使用这个字符串将其作为环境变量;但是 setenv,函数内部会将用户传入的字符串拷贝到自己的缓冲区中
-
在环境变量存在的情况下,putenv,是直接强制修改该变量的值;而对应setenv函数,用于可以通过第三个参数控制是否修改现有环境变量的值
-
-
-
-
执行程序时添加环境变量
-
比如说我在执行当前目录下面这个test程序的时候。前面添加了一个name=value这个字符串。其实这个就表示,我们要将这个环境变量传给这个进程
-
name=value ./test
-
-
从环境变量表中移除参数 name 标识的环境变量
- #include <stdlib.h>
int unsetenv(const char *name);
- #include <stdlib.h>
清空环境变量
-
可以通过将全局变量 environ 赋值为 NULL 来清空所有变量
-
通过 clearenv()函数
- #include <stdlib.h>
int clearenv(void);
- #include <stdlib.h>
创建子进程
所有进程都是由其父进程所创建出来的
-
譬如我们在终端下执行某个应用程序: ./test
-
这个程序启动之后就是一个进程,这个进程就是由它的父进程(也就是这个shell 进程)所创建出来的
-
shell 进程就是 shell 解析器(shell 解析器有很多种,譬如 bash、sh 等),所谓解析器就是解析用户输入的各种命令,然后做出相应的响应,执行相应的程序
-
存在一个最原始的父进程(所有进程的“祖先”),这个祖先进程就是 init 进程。init 进程的 PID 是 1,它是所有子进程的父进程,所有进程的祖先,一切从 init 开始!
进程空间的概念
-
在 Linux 系统中,进程与进程之间、进程与内核之间都是相互隔离的,各自在自己的进程空间中运行(内核就是在自己的内核空间中运行);一个进程不能读取或修改另一个进程或内核的内存数据,这样提高了系统的安全性与稳定性
-
新进程被创建出来之后,便是一个独立的进程,拥有自己独立的进程空间,拥有自己唯一的进程号(PID),拥有自己独立的 PCB(进程控制块),新进程会被内核同等调度执行,参与到系统调用中
fork创建子进程
- #include <unistd.h>
pid_t fork(void);
如何理解fork系统调用
-
一次fork调用会产生两次返回值
-
fork调用会创建一个新的子进程。
-
调用后,系统中存在父进程和子进程两个独立的进程
-
fork调用会有两次返回值,一次在子进程中,一次在父进程中
-
子进程的返回值是0,父进程的返回值是一个大于0的整数
-
通过返回值可以判断当前是子进程还是父进程
-
父进程返回的大于0的整数实际上是子进程的进程ID(pid)
-
-
fork创建了一个与原来进程几乎完全相同的进程
-
子进程是父进程的副本,继承了父进程的数据段、堆、栈和打开的文件描述符
-
子进程复制了父进程的存储空间,包括数据段、堆和栈
-
子进程和父进程的存储空间是独立的,不共享
-
子进程和父进程可以独立修改各自的栈数据和堆段变量,互不影响
-
-
子进程从fork调用返回后的代码开始运行
- 虽然子进程和父进程运行在不同的进程空间中,但是
他们执行的却是同一个程序
- 虽然子进程和父进程运行在不同的进程空间中,但是
父、子进程
父、子进程间的文件共享
- 子进程复制了父进程的文件描述符表
- 父进程和子进程的文件描述符指向同一个文件表
- 意味着它们指向磁盘上相同的文件
- 父进程和子进程共享这些文件
- 子进程对文件偏移量的更新会影响父进程中相应文件描述符的偏移量
父、子进程间的竞争关系
-
fork之后父进程、子进程谁先运行
-
结论:不确定,但是绝大数情况下是父进程先返回
-
那如何明确保证某一特性执行顺序呢?这个时候可以通过采用采用某种同步技术来实现
- 譬如利用信号,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它
监视子进程
父进程监视子进程
-
父进程需要知道子进程状态什么时候发送改变
-
子进程状态改变包括
-
子进程终止
-
子进程因为收到停止信号而停止运行,SIGSTOP、SIGTSTP
-
子进程在停止状态下因为收到恢复信号而恢复运行,SIGCONT
-
wait函数
-
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); -
status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息
- 参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,
可以通过以下宏来检查 status 参数:
1、WIFEXITED(status)宏用于检查子进程是否正常终止,如果是则返回true。
2、WEXITSTATUS(status)宏用于获取子进程的退出状态,该状态是子进程调用_exit()或exit()时指定的数值。
3、WIFSIGNALED(status)宏用于检查子进程是否被信号终止,如果是则返回true。
4、WTERMSIG(status)宏用于获取导致子进程终止的信号编号。
5、WCOREDUMP(status)宏用于检查子进程终止时是否产生了核心转储文件,如果是则返回true
- 参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,
-
返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1
-
监视子进程什么被终止,以及获取子进程终止时的状态信息
-
回收子进程的一些资源,俗称为子进程”收尸"
- 一次wait调用只能替一个已经终止的子进程”收尸"
waitpid函数
-
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options); -
pid:参数 pid 用于表示需要等待的某个具体子进程
- 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
⚫如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程
⚫如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程
⚫如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价
- 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
-
status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息
- 参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,
可以通过以下宏来检查 status 参数:
1、WIFEXITED(status)宏用于检查子进程是否正常终止,如果是则返回true。
2、WEXITSTATUS(status)宏用于获取子进程的退出状态,该状态是子进程调用_exit()或exit()时指定的数值。
3、WIFSIGNALED(status)宏用于检查子进程是否被信号终止,如果是则返回true。
4、WTERMSIG(status)宏用于获取导致子进程终止的信号编号。
5、WCOREDUMP(status)宏用于检查子进程终止时是否产生了核心转储文件,如果是则返回true
- 参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,
-
options:是一个位掩码,可以包括 0 个或多个如下标志
- 1、WNOHANG参数使得wait()函数在没有子进程状态改变(如终止或暂停)时立即返回,实现非阻塞等待,可以实现轮训 poll,返回值为0表示没有子进程状态改变。
2、WUNTRACED参数使得wait()函数不仅返回终止的子进程状态信息,还返回因信号而停止(暂停运行)的子进程状态信息。
3、WCONTINUED参数使得wait()函数返回那些因收到SIGCONT信号而恢复运行的子进程的状态信息。
- 1、WNOHANG参数使得wait()函数在没有子进程状态改变(如终止或暂停)时立即返回,实现非阻塞等待,可以实现轮训 poll,返回值为0表示没有子进程状态改变。
-
返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0
waitid函数
SIGCHLD信号
介绍
-
子进程终止
-
子进程因为收到停止信号而停止运行,SIGSTOP、SIGTSTP
-
子进程在停止状态下因为收到恢复信号而恢复运行,SIGCONT
使用异步方式来回收子程序:SIGCHLD信号处理函数
-
当信号处理函数被调用时,会临时将该信号加入进程的信号掩码中,这可能导致在处理SIGCHLD信号时,父进程无法捕获到所有子进程终止产生的SIGCHLD信号
-
由于父进程的SIGCHLD信号处理函数每次只调用一次wait(),可能会遗漏处理某些僵尸进程
-
解决方案是在SIGCHLD信号处理函数中使用非阻塞方式循环调用waitpid(),直到没有更多终止的子进程需要处理
-
通常的SIGCHLD信号处理函数代码如下:
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
这会持续循环直到waitpid()返回0(无僵尸进程)或-1(错误发生) -
故应在创建子进程之前为SIGCHLD信号绑定处理函数
僵尸进程与孤儿进程
讨论:子进程与父进程谁先终止?
-
父进程先于子进程结束
-
子进程先于父进程结束
孤儿进程
-
当父进程先于子进程结束时,子进程成为“孤儿”进程
-
在Linux系统中,所有孤儿进程自动成为init进程(进程号为1)的子进程
-
孤儿进程调用getppid()将返回1,表示init进程成为其新的父进程
-
通过检查子进程的父进程ID(getppid()的返回值),可以判断其“生父”是否仍在运行
-
Ubuntu 系统图形化界面下的一个后台守护进程,可负责“收养”孤儿进程,所以图形化界面下,后台守护进程就自动成为了孤儿进程的父进程
僵尸进程
-
进程结束时,父进程需调用wait()等函数回收子进程资源,以避免资源浪费
-
若子进程先于父进程结束且未被回收,则成为僵尸进程,占用系统资源但不再执行任务
-
僵尸进程的命名源于其状态类似于“曝尸荒野”
-
父进程调用wait()后,僵尸进程会被内核删除;若父进程未调用wait()而退出,init进程会接管并清理僵尸进程
-
父进程未回收已结束的子进程会导致僵尸进程累积,可能填满内核进程表,影响新进程创建
-
僵尸进程无法通过信号杀死,需杀死其父进程或等待父进程终止,由init进程接管并清理
-
程序设计应监控子进程状态,及时调用wait()回收终止的子进程,防止僵尸进程产生
执行新程序
execve系统调用
-
系统调用execve()用于将新程序加载到现有进程中,替换原有程序及其数据结构,从新程序的main()函数开始执行。
-
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);-
filename:指向新程序的路径名,可以是绝对或相对路径
-
argv:命令行参数数组,对应于main()函数的argv参数,以NULL结束,argv[0]为新程序路径名
-
envp:环境变量数组,对应于新程序的environ数组,以NULL结束,格式为name=value
-
返回值:成功时不返回,失败时返回-1并设置errno
-
-
将子进程代码独立成可执行文件,通过exec操作加载,提高灵活性和扩展性
exec族库函数
-
exec 族函数中的库函数
都是基于系统调用 execve()而实现的,虽然参数各异、但功能相同,包括:execl()、execlp()、execle()、execv()、execvp()、execvpe() -
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char arg, … / (char *) NULL */);
int execlp(const char *file, const char arg, … / (char *) NULL */);
int execle(const char *path, const char arg, … /, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
system库函数
-
system()函数可以很方便地在我们的程序当中执行任意 shell 命令
- 首先 system()
会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程),并通过 shell 执行参数
command 所指定的命令
- system 函数执行一个shell 命令,system 函数内部至少会创建两个进程
- 1、system 函数创建的 shell 子进程
- 2、shell 子进程执行命令时所创建的子进程(一个或多个,具体依执行的命令而定!)
-
#include <stdlib.h>
int system(const char *command);-
command: 参数 command 指 向 需 要 执 行的 shell 命令,以字符串的形式提供,譬如"ls -al"、 "echo
HelloWorld"等 -
返回值:
1、当command为NULL时,system()返回非0值(shell可用)或0(shell不可用)
2、若无法创建子进程或获取其终止状态,system()返回-1
3、若子进程无法执行shell,system()返回类似_exit(127)的状态
4、若所有系统调用成功,system()返回执行command的shell进程的终止状态
-
-
system()函数简化了进程管理的复杂性,但效率较低,因为它至少创建两个进程来执行命令,不适合对效率有高要求的场景
vfork系统调用
fork函数的使用场景
-
父进程希望子进程复制自己,父、子进程执行相同的程序,各自在自己的进程空间中运行
-
子进程执行一个新的程序,从该程序的main函数开始运行,调用exec函数
fork函数的缺点
-
fork()系统调用复制父进程的数据段、堆段和栈段,导致效率降低和资源浪费
-
子进程通常会调用exec函数,不再使用父进程的数据段、堆段和栈段,进一步加剧了效率问题
-
现代Linux系统采用写时复制(copy-on-write)技术来优化fork()的性能,减少不必要的复制
vfork函数引入
-
这个函数主要是针对 fork 函数的缺点而引入,所以它的使用场景自然也是在子进程中执行exec 调用加载外部的一个新程序,从新程序的 main 函数开始运行
-
最终目标是一样的,都是创建一个进程;并且返回值也是一样的。它们两个都是系统调用
vfork函数与fork函数之间的主要区别
-
vfork()不复制父进程地址空间,子进程共享父进程内存,提高了效率,但可能导致未知结果如果子进程修改父进程数据或未调用exec或_exit
- 注意:vfork 创建的子进程如果要终止应调用_exit,而不能调用 exit 或 return 返回,因为如果子进程调用 exit 或 return 终止,则会调用父进程绑定的终止处理函数以及刷新父进程的 stdio 缓冲,影响到父进程
-
vfork()保证子进程先运行,父进程在子进程调用exec后才可能运行,这可能导致难以察觉的程序错误
- 注意:如果子进程在终止或成功调用 exec 函数之前,依赖于父进程的进一步动作,将会导致死锁!
-
尽管
vfork()
效率高于fork()
,但现代Linux系统已通过写时复制技术优化了fork()
,建议在非绝对速度关键场合使用fork()
而非vfork()
进程状态与进程关系
进程状态有哪些
-
Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态
-
R(TASK_RUNNING):运行状态或可执行状态(就绪态)
正在运行的进程或者在进程队列中等待运行的进程都处于该状态,所以该状态实际上包含了运行态和就绪态这两个基本状态。处于就绪态的进程,表示它已经处于准备运行状态,一旦得到CPU 使用权就会进入到运行态 -
运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态
-
S(TASK_INTERRUPTIBLE):可中断睡眠状态(浅度睡眠)
可中断睡眠状态也被称为浅度睡眠状态,处于这个状态的进程由于在等待某个事件(等待资源有效)而被系统挂起,譬如等待IO事件、主动调用 sleep 函数等。一旦资源有效时就会进入到就绪态,当然该状态下的进程也可被信号或中断唤醒 -
D(TASK_UNINTERRUPTIBLE):不可中断睡眠状态(深度睡眠)
不可中断睡眠状态也被称为深度睡眠状态,该状态下的进程也是在等待某个事件、等待资源有效,一旦资源有效就会进入到就绪态;与浅度睡眠状态的区别在于,深度睡眠状态的进程不能被信号或中断唤醒,只有当它所等待的资源有效时才会被唤醒。一般该状态下的进程正在跟硬件交互、交互过程不允许被其它进程中断! -
T(TASK_STOPPED):停止状态(暂停状态)
当进程收到停止信号时(譬如 SIGSTOP、SIGTSTP 等停止信号),就会由运行状态进入到停止状态。当处于停止状态下,收到恢复信号(譬如 SIGCONT 信号)时就会进入到就绪态 -
Z(TASK_ZOMBIE):僵尸状态
表示进程已经终止了,但是并未被其父进程所回收,也就是进程已经终止、但并未彻底消亡需要其父进程回收它的一些资源,归还系统,然后该进程才会从系统中彻底删除!
这些进程状态之间的转换关系
-
ps命令查看到的进程状态信息中,除了第一个大写字母用于表示进程当前所处的状态之外还有其他的一些字符,譬如s、!、N、+、<等
s:表示当前进程是一个会话的首领进程
!:表示当前进程是一个多线程进程
N:表示低优先级
<:表示高优先级
+:表示当前进程处于前台进程组中 -
X(TASK DEAD):死亡状态
此状态非常短暂、ps.命令捕捉不到。处于此状态的进程即将被彻底销毁!可以认为就是僵尸进程被回收之后的一种状态
进程间关系有哪些
-
主要包括:无关系(相互独立)、父子进程关系、进程组以及会话
-
1、无关系
两个进程间没有任何关系,相互独立 -
2、父子进程关系
两个进程通过fork()创建形成父子关系,父进程调用fork()创建子进程。如果父进程先结束,init进程(系统初始化进程)会接管子进程,成为其新的父进程 -
3、进程组
①每个进程有进程ID、父进程ID和进程组ID,进程组是相关进程的集合,用于标识和管理
②进程组内的进程可能存在父子或兄弟关系,或在功能上有联系
③Linux系统使用进程组来简化进程管理,例如,可以通过终止进程组来一次性终止多个相关进程,提高了操作效率并减少了错误风险-
关于进程组需要注意以下以下内容:
⚫每个进程必定属于某一个进程组、且只能属于一个进程组;
⚫每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID;
⚫在组长进程的 ID 前面加上一个负号即是操作进程组;
⚫组长进程不能再创建新的进程组;
⚫只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关;
⚫一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离
开该进程组;
⚫默认情况下,新创建的进程会继承父进程的进程组 ID。 -
通过系统调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID
-
#include <unistd.h>
pid_t getpgid(pid_t pid);
pid_t getpgrp(void); -
getpgrp()无参数,返回调用进程的进程组ID
-
getpgid(pid)通过参数pid获取指定进程的进程组ID,pid为0时返回调用进程的进程组ID
-
getpgid()成功返回进程组ID,失败返回-1并设置errno
-
getpgrp()等价于getpgid(0)
-
-
调用系统调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组
-
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void); -
setpgid(pid, gpid)设置指定进程的进程组ID,pid==gpid时创建新进程组并成为组长
-
pid为0时使用调用者进程ID,gpid为0时创建新进程组
-
setpgrp()等价于setpgid(0, 0)
-
进程只能为自己或子进程设置进程组ID,子进程调用exec后无法更改其进程组ID
-
-
-
4、会话
会话是一个或多个进程组的集合-
每个进程组必定属于某一个会话,并且只能在一个会话中
-
一个会话包含一个或多个进程组,最多只能有一个前台进程组(前台作业)、其它的都是后台进程组(后台作业)
-
每个会话都有一个会话首领(leader,首领进程),即创建会话的进程(会话的首领进程就是创建该会话的进程)
-
同样每个会话也有 ID 标识,称为会话 ID(简称:SID),每个会话的 SID 就是会话首领进程的进程 ID(PID)。所以如果两个进程的 SID 相同,表示它们俩在同一个会话中。在应用程序中调用 getsid 函数获取进程的 SID
-
#include <unistd.h>
pid_t getsid(pid_t pid) -
参数pid为0时,getsid()返回调用者进程的会话ID
-
参数pid不为0时,返回指定进程的会话ID
-
成功时返回会话ID,失败时返回-1并设置errno
-
-
会话的生命周期从会话创建开始、直到会话中所有进程组生命周期结束!与会话首领进程是否终止无关!
-
一个会话可以有控制终端、也可没有控制终端,每个会话最多只能连接一个控制终端控制终端与会话中的所有进程相关联、绑定,控制、影响着会话中所有进程的一些行为特性譬如控制终端产生的信号,将会发送给该会话中的进程(譬如CTRL+C、CTRL+Z、CTRL+\产生的中断信号、停止信号、退出信号,将发送给前台进程组);譬如前台进程可以通过终
端与用户进行交互、从终端读取用户输入的数据,进程产生的打印信息会通过终端显示出来譬如当控制终端关闭的时候,会话中的所有进程也被终止! -
当我们在 Ubuntu 系统中打开一个终端,那么就创建了一个新的会话(shell 进程就是这个会话的首领进程,也就意味着该会话的 SID 等于 shell 进程的 PID),打开了多少个终端
其实就是创建了多少个会话 -
默认情况下,新创建的进程会继承父进程的会话 ID,子进程与父进程在同一个会话中(也可以说子进程继承了父进程的控制终端)
-
关于前台与后台的一些操作
-
执行程序时,后面添加&使其在后台运行
-
fg命令将后台进程调至前台继续运行
-
CTRL+Z将一个前台运行的进程调至后台、并处于停止状态(暂停状态)
-
前台进程组中的所有进程都是前台进程。所以终端产生的信号它们都会接收到、譬如CTRL+C、CTRL+Z、CTRL+\
-
-
系统调用 setsid()可以创建一个会话
-
使用ps -ajx可以查看进程的控制终端
-
TTV = ?则表示该进程没有控制终端
-
Linux系统中守护进程就没有控制终端
-
-
#include <unistd.h>
pid_t setsid(void); -
调用者非进程组组长时,setsid()创建新会话并成为会话首领和进程组组长,无控制终端
-
成功时返回新会话ID,失败时返回-1并设置errno
-
-
fork后,父进程信号处理机制对子进程的影响
父进程信号处理函数对子进程的影响
- fork后子进程会继承父进程绑定的信号处理函数,若调用exec 加载新程序后,就不会在继承这个信号处理函数
父进程信号掩码对子进程的影响
- fork后子进程会继承父进程的信号掩码,执行exec后仍会继承这个信号掩码
守护进程
何为守护进程
-
守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性
地执行某种任务或等待处理某些事情的发生 -
长期运行
- 守护进程长期运行,从系统启动到关机持续运行,不受用户登录注销影响
-
与控制终端脱离
- 守护进程与控制终端脱离,避免终端关闭或信息打断进程,适用于后台任务和系统服务
-
守护进程常用于实现服务器(如inetd, httpd)和系统任务(如crond),进程名常带"d"标识
-
守护进程独立于终端,自成进程组和会话,通过ps -ajx命令查看
编写守护进程程序
-
守护进程的重点在于脱离控制终端
-
- 创建子进程、终止父进程
父进程通过fork()创建子进程,然后退出,使子进程独立,确保子进程不是进程组组长,为调用setsid()做准备
- 创建子进程、终止父进程
-
- 子进程调用 setsid 创建会话
子进程调用setsid()创建新会话:子进程调用setsid()创建新会话,成为会话首领和进程组组长,脱离原会话、进程组和控制终端
- 子进程调用 setsid 创建会话
-
- 将工作目录更改为根目录
更改工作目录为根目录:将子进程的工作目录改为根目录,避免文件系统卸载问题
- 将工作目录更改为根目录
-
- 重设文件权限掩码 umask
将文件权限掩码设为0,确保子进程有最大文件操作权限
- 重设文件权限掩码 umask
-
- 关闭不再需要的文件描述符
关闭子进程继承的所有文件描述符,释放资源
- 关闭不再需要的文件描述符
-
- 将文件描述符号为 0、1、2 定位到/dev/null
将标准输入、输出和错误重定向到/dev/null,避免输出和输入问题
- 将文件描述符号为 0、1、2 定位到/dev/null
-
- 其它:忽略 SIGCHLD 信号
设置忽略SIGCHLD信号,避免僵尸进程并减轻服务器负担
-
在 Linux 下,可以将 SIGCHLD 信号的处理方式设置为
SIG_IGN,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程、又省去了服务器进程回收子进程所占用的时间 -
虽然 SIGCHLD 信号的系统默认操作就是将其忽略,但显示设置忽略该信号,会让系统执行上面的一个操作;在这方面,系统对 SIGCHLD 信号的处理非常独特(系统对这个SIGCHLD 信号的一个特殊的处理),不同于其他信号!!!
- 其它:忽略 SIGCHLD 信号
SIGHUP 信号
-
会话退出时发送SIGHUP信号:用户退出会话时,系统发送SIGHUP信号给会话及其所有子进程
-
子进程接收SIGHUP信号后终止:子进程默认处理SIGHUP信号的方式是终止进程,导致所有子进程退出
-
会话终止条件:所有会话中的进程退出后,会话随之终止
-
SIGHUP信号的默认处理:程序通常不对SIGHUP信号进行特殊处理,默认处理方式是终止进程,影响前台和后台进程
-
忽略SIGHUP信号后,进程不会因终端退出而终止,因为会话结束需要所有进程退出,忽略SIGHUP信号的进程不终止,会话因此继续存在
单例模式运行
程序通常允许多次执行,产生多个实例(进程),如聊天软件和游戏。但某些程序设计为单例模式,只允许一次运行,如守护进程,它们在系统运行期间提供服务,多次运行无意义且可能引发错误。
通过文件存在与否进行判断
-
使用文件作为标志:程序启动时检查特定文件是否存在,以判断进程是否已在运行
-
文件存在时的处理:如果文件存在,说明进程已在运行,程序应立即退出
-
文件不存在时的处理:如果文件不存在,说明进程未运行,程序应创建该文件
-
程序结束时的操作:程序结束时删除该文件
-
文件命名建议要求:特定文件的命名应独特,以防在文件系统中意外存在
-
存在很大的问题
-
程序异常退出
程序异常同样无法执行到进程终止处理函数 delete_file(),同样将导致无法删除这个特定的文件-
异常退出的处理:进程因接收信号而异常终止时,可通过设置信号处理方式来解决
-
忽略信号:设置信号处理方式为忽略信号,使进程在接收到信号时不会终止
-
注册信号处理函数:针对特定信号(如SIGTERM、SIGINT)注册处理函数,在函数中删除文件并退出进程
-
不可忽略或捕获的信号:存在一些信号(如SIGKILL和SIGSTOP)无法被忽略或捕获,因此上述方法不完全可靠
-
-
-
所以使用这种方法实现单例模式运行并不靠谱
使用文件锁
-
程序启动操作:程序启动后,首先尝试打开或创建一个文件,使用O_WRONLY | O_CREAT标志
-
文件锁获取:程序尝试获取文件锁,如果成功,将进程ID(PID)写入文件
-
文件锁保持:成功获取锁后,程序不关闭文件或解锁,保持文件锁以防止其他实例运行
-
锁获取失败处理:如果获取文件锁失败,说明程序已在运行,当前启动应退出
-
文件锁自动释放:程序退出或文件关闭时,文件锁会自动释放
-
通过系统调用
flock()、fcntl()或库函数 lockf()均可实现对文件进行上锁- 系统调用 flock()
产生的是咨询锁(建议性锁)、并不能产生强制性锁
- 系统调用 flock()