Linux编程学习笔记-多进程编程
子进程
fork函数的用法
Linux可以通过fork()
函数在主进程中创建子进程,进程的创建实例如下:
#include <iostream>
#include <unistd.h>
int main(int argc, char * argv[]) {
pid_t f_pid;
std::cout << "开始执行程序." << std::endl;
f_pid = fork();
if (f_pid < 0) {
std::cout << "启动子进程失败." << std::endl;
} else if (f_pid == 0) {
std::cout << "子进程中 pid = " << getpid() << " f_pid = " << f_pid << std::endl;
}
else if (f_pid > 0) {
std::cout << "父进程中 pid = " << getpid() << " f_pid = " << f_pid << std::endl;
}
while (true) {
sleep(2);
std::cout << "进程运行 pid = " << getpid() << std::endl;
}
return 0;
}
开始执行程序 pid = 1162
父进程中 pid = 1162 f_pid = 1163
子进程中 pid = 1163 f_pid = 0
进程运行 pid = 1162
进程运行 pid = 1163
进程运行 pid = 1162
进程运行 pid = 1163
进程运行 pid = 1162
进程运行 pid = 1163
开始执行程序
这句话仅仅在父进程
中打印了一次,在子进程中只打印了fork()
函数后边的内容。
僵尸进程
kill
子进程时,Linux内核不会将其完全回收还会保留进程的一些资源,但不会运行该进程,此时子进程称为僵尸进程
。
# 进程状态
~$ ps -eo pid,ppid,sid,tty,pgrp,comm,stat | grep -E 'bash|PID|LinuxCode|Z'
PID PPID SID TT PGRP COMMAND STAT
858 857 858 pts/0 858 bash Ss
905 904 905 pts/1 905 bash Ss
953 858 858 pts/0 953 LinuxCode S+
954 953 858 pts/0 953 Linux <defunct> Z+ # 僵尸进程
僵尸进程的STAT
位显示为"Z"
。
处理僵尸进程
僵尸进程在父进程停止时会自动销毁,这是一种处理方式。另外就是在主进程中捕获SIGCHLD
信号,然后进行相关处理。在子进程销毁时,会向父进程发送SIGCHLD
信号,演示效果如下:
# 父进程附件trace命令
~$ sudo strace -e trace=signal -p 1502
strace: Process 1502 attached
# 杀死子进程
~$ kill -9 1503
# 父进程收到信号
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=1503, si_uid=1000, si_status=SIGKILL, si_utime=0, si_stime=0} ---
在代码中添加信号处理函数,在函数中使用waitpid()
函数对该信号进行处理。
#include <iostream>
#include <unistd.h> // fork
#include <csignal> // 信号值
#include <sys/wait.h> // waitpid
void handle_singal(int _si_signo)
{
/* 子进程状态 */
int status = 0;
if (_si_signo == SIGCHLD) {
std::cout << "收到了 SIGCHLD 信号,pid = " << getpid() << std::endl;
pid_t pid = waitpid(-1, &status, WNOHANG);
std::cout << "执行结果 = " << pid << std::endl;
std::cout << "状态数值 = " << status << std::endl;
}
}
int main(int argc, char * argv[]) {
pid_t f_pid;
std::cout << "开始执行程序 pid = " << getpid() << std::endl;
// 给系统注册捕获信号的回调函数
if(signal(SIGCHLD, handle_singal) == SIG_ERR) {
std::cout << "无法捕捉 SIGCHLD 信号." << std::endl;
exit(1);
}
f_pid = fork();
while (true) {
sleep(2); //休息1秒
std::cout << "进程运行 pid = " << getpid() << std::endl;
}
return 0;
}
~/LinuxCode/temp$ ./LinuxCode
开始执行程序 pid = 1052
父进程中 pid = 1052 f_pid = 1053
子进程中 pid = 1053 f_pid = 0
进程运行 pid = 1052
进程运行 pid = 1053
进程运行 pid = 1052
进程运行 pid = 1053
进程运行 pid = 1052
# 使用 kill -2 1052 杀掉子进程
收到了 SIGCHLD 信号,pid = 1052
执行结果 = 1053
状态数值 = 2
waitpid函数原型
pid_t waitpid(pid_t pid,int *status,int options);
参数 | 说明 |
---|---|
pid | 不同于返回值的pid,控制等待子进程的方式 |
status | 存放子进程结束的状态(信号),例如,用型号2结束,则该数值为2 |
options | 控制waitpid的额外选项 |
如果执行成功则返回pid值,若失败则返回-1。
- pid参数说明
参数 | 说明 |
---|---|
pid < -1 | 等待指定 进程组pid相同的任何子进程(使用pid绝对值)。 |
pid = -1 | 等待任何子进程 |
pid = 0 | 等待与当前进程 组pid相同的任何子进程 |
pid > 0 | 等待指定 pid的子进程。 |
- options参数说明
参数 | 说明 |
---|---|
WNOHANG | 指定pid子进程没有结束,waitpid返回,返回值0 ,不阻塞 |
WUNTRACED | 指定pid子进程进入暂停状态,waitpid返回,返回pid,阻塞 |
fork函数特性
- fork函数产生了一个和当前进程完全一样的新进程,并和当前进程一样从fork函数返回。
- fork函数产生新进程的
速度非常快
,因为fork产生的新进程并不复制原进程的内存空间
,而是和原进程共享一个内存空间
,这个内存空间的特性是写时复制
,当进程对内存进行修改时则将该内存进行独立。
#include <iostream>
#include <unistd.h> // fork
int count = 0;
int main(int argc, char * argv[]) {
pid_t f_pid;
std::cout << "开始执行程序 pid = " << getpid() << std::endl;
f_pid = fork();
if (f_pid == 0) {
while (true) {
std::cout << "子进程运行 pid = " << getpid() << " count = " << count++ << std::endl;
sleep(1);
}
} else {
while (true) {
std::cout << "父进程运行 pid = " << getpid() << " count = " << count++ << std::endl;
sleep(3);
}
}
return 0;
}
开始执行程序 pid = 1149
父进程运行 pid = 1149 count = 0
子进程运行 pid = 1153 count = 0
子进程运行 pid = 1153 count = 1
子进程运行 pid = 1153 count = 2 # 子进程到 2
父进程运行 pid = 1149 count = 1 # 父进程到 1
子进程运行 pid = 1153 count = 3
子进程运行 pid = 1153 count = 4
子进程运行 pid = 1153 count = 5
守护进程
特点
普通进程都是基于控制终端
运行的,当终端关闭时进程也随之关闭。而守护进程则是独立于控制终端
并且守护进程因该是周期性地执行任务
。守护进程具体特点如下:
-
守护进程都具有
超级用户
的权限。 -
守护进程的父进程
是init进程
。 -
守护进程都不占用
控制终端
,用ps axj
命令可以看到,在TTY
列都是?
。~$ ps axj PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 0 2 0 0 ? -1 S 0 0:00 [kthreadd] 2 3 0 0 ? -1 S 0 0:00 [ksoftirqd/0] 2 4 0 0 ? -1 S 0 0:01 [kworker/0:0] 2 5 0 0 ? -1 S< 0 0:00 [kworker/0:0H] 2 7 0 0 ? -1 S 0 0:00 [rcu_sched]
其中,带
[]
的表示内核守护进程。
编写规则
- 创建子进程,父进程退出。父进程退出后可以脱离终端,让子进程继续运行,同时,由于父进程退出子进程会自动被
init
进程接管。 - 调用
setsid
函数创建会话。使用setsid
函数让子进程摆脱原进程组的控制
、摆脱原会话控制
以及摆脱原控制终端控制
。 - 设置文件权限。把文件权限设置成最大权限类似于
777
。 - 输入输出重定向。由于守护进程是在后台运行,因此不应该从键盘上接收任何东西,也不应该把输出结果打印到屏幕或者终端上来。因此,一般是将守护进程的
标准输出
以及标准输入
重定向到空设备(/dev/null)
。
#include <iostream>
#include <unistd.h> // fork
#include <sys/wait.h> // waitpid
#include <sys/stat.h> // umask
#include <fcntl.h> // open
int init_daemon()
{
/* 创建子进程 */
pid_t f_pid;
f_pid = fork();
if (f_pid > 0) {
/* 让父进程退出 */
exit(0);
} else if (f_pid == -1) {
return -1;
}
/* 给子进程创建独立会话 */
if (setsid() == -1) {
return -1;
}
/* 设置超级权限 */
umask(0);
/* 关闭输入输出流 */
int fd = open("/dev/null", O_RDWR);
if (fd == -1) {
return -1;
}
/* 将标准输入流指向,空设备,不接受键盘等输入 */
if (dup2(fd, STDIN_FILENO) == -1) {
return -1;
}
/* 将标准输出流指向,空设备,不输出内容到控制台 */
if (dup2(fd, STDOUT_FILENO) == -1) {
return -1;
}
/* 关闭 空设备 文件描述符 */
if (fd > STDERR_FILENO) {
if (close(fd) == -1) {
return -1;
}
}
return 1;
}
int main(int argc, char * argv[]) {
if (init_daemon() != 1) {
return 1;
}
while (true) {
sleep(1);
std::cout << "休息中 " << getpid() << std::endl;
}
return 0;
}
其中dup2()
表示将参数二所指向的文件描述符
重定向给参数一所指向的文件描述符
。由于Linux系统中有三个特殊文件描述符STDIN_FILENO
、STDOUT_FILENO
和STDERR_FILENO
,这三个文件描述符分别对应了0
、1
、2
三个数字,再打开其它设备或文件均大于这三个数,因此,最后一步是将文件描述符大于STDERR_FILENO
的,也就是前边打开的空设备
关闭。
程序运行结果:
$ ps -eo pid,ppid,sid,tty,pgrp,comm,stat | grep -E 'bash|PID|LinuxCode|Z'
PID PPID SID TT PGRP COMMAND STAT
1018 1017 1018 pts/0 1018 bash Ss
1034 1 1034 ? 1034 LinuxCode Ss
多进程的区分
当一个主进程启动了多个子进程之后,无论是主进程还是子进程在ps
中COMMAND
列显示的都是同一个名称
# 注意这里需要将 comm 列显示环境变量,改为 cmd 或者 args
$ ps -eo pid,ppid,sid,tty,pgrp,args,stat | grep -E 'bash|PID|LinuxCode'
PID PPID SID TT PGRP COMMAND STAT
1072 1071 1072 pts/1 1072 -bash Ss
1164 1163 1164 pts/0 1164 -bash Ss+
1648 1647 1648 pts/2 1648 -bash Ss
1664 1648 1648 pts/2 1664 ./LinuxCode S+
1665 1664 1648 pts/2 1664 ./LinuxCode S+
这种情况下除了分析PID
和PPID
的关系外,基本无法区别哪个是子进程哪个是父进程。这种情况下可以修改主进程和子进程的名称加以区分。
名称由来
在上面使用ps
命令输出之后COMMAND
显示的是./LinuxCode
这个是启动程序时的命令。程序的启动可以追加参数,就像ps -eo
命令。
$ ./LinuxCode u 1 tset
~$ ps -eo pid,ppid,sid,tty,pgrp,args,stat | grep -E 'bash|PID|Linux'
PID PPID SID TT PGRP COMMAND STAT
1072 1071 1072 pts/1 1072 -bash Ss
1648 1647 1648 pts/2 1648 -bash Ss
1801 1648 1648 pts/2 1801 ./LinuxCode u 1 tset S+
1802 1801 1648 pts/2 1801 ./LinuxCode u 1 tset S+
此时,可以看出COMMAND
字段显示已经变成了启动程序时的完整命令,因此,可以判断启动命令等价于COMMAND
字段显示。
修改原理
程序的启动命令实际在main函数的参数中可以获取
。main函数有两个传参第一个参数是附加命令的个数
,第二个参数是具体参数的字符串值
。
#include <iostream>
int main(int argc, char * argv[]) {
std::cout << "参数个数 = " << argc << std::endl;
for (int index = 0; index < argc; ++index) {
std::cout << argv[index] << std::endl;
}
return 0;
}
$ ./LinuxCode u 1 tset
参数个数 = 4
./LinuxCode
u
1
tset
由此可以简单的认为在显示程序的标题的时候就是使用argv
的值拼接而成的,如果将argv的值修改了就可以修改标题的显示
。
#include <iostream>
int main(int argc, char * argv[]) {
std::cout << "参数个数 = " << argc << std::endl;
for (int index = 0; index < argc; ++index) {
std::cout << argv[index] << std::endl;
}
/* 拷贝字符串覆盖原内容 */
strcpy(argv[0], "Linux process name");
return 0;
}
~$ ps -eo pid,ppid,sid,tty,pgrp,args,stat | grep -E 'bash|PID|Linux'
PID PPID SID TT PGRP COMMAND STAT
1072 1071 1072 pts/1 1072 -bash Ss
1648 1647 1648 pts/2 1648 -bash Ss
1952 1648 1648 pts/2 1952 Linux process name t S+
1953 1952 1648 pts/2 1952 Linux process name t S+
从运行结果上看,程序启动的./LinuxCode u 1 tset
由于被覆盖的问题已经只剩下t
了,修改标题第一步成功。上面的程序已经将后续的参数都覆盖了,如果名称太长还会覆盖系统环境变量
内存的使用。
环境变量内存
系统环境变量所占用的内存是紧挨着
程序变量的。
#include <iostream>
#include <unistd.h>
int main(int argc, char * argv[]) {
printf("argv [%d] 地址=%p\n", argc - 1, *argv);
printf("argv [%d] 内容=%s\n", argc - 1, argv[argc - 1]);
printf("environ [%d] 地址=%p\n", 0, *environ);
printf("environ [%d] 内容=%s\n", 0, environ[0]);
printf("environ - argv 地址=%ld\n", (*environ) - (*argv));
printf("argv [%d] 长度=%zu\n", argc - 1, strlen(argv[argc - 1]) + 1);
return 0;
}
$ ./LinuxCode
argv [0] 地址=0x7ffc32a8782f
argv [0] 内容=./LinuxCode
environ [0] 地址=0x7ffc32a8783b
environ [0] 内容=XDG_SESSION_ID=3
environ - argv 地址=12
argv [0] 长度=12
0x7ffc32a8782f
和0x7ffc32a8783b
之间正好相差./LinuxCode
的长度加1
,其中加1表示数组最后的\0
统计长度时默认不包含。这说明不能盲目的将argv
的参数替换成想要的字符串,因为名称过长会覆盖环境变量。
修改方式
-
为了不破坏环境变量需要先将环境变量的地址指向新开辟的内存的地址,以此来防止系统环境变量被破环。
extern char* g_os_env; // 系统环境变量数组内存起始位置-新 extern int g_environ_len; // 系统环境变量数组长度 void copy_env_buffer() { /* 记录环境变量的长度 */ for (int index = 0; environ[index] != nullptr; ++index) { g_environ_len += std::strlen(environ[index]) + 1; } /* 申请环境变量的新内存,并初始化内存数据为 0 */ g_os_env = new char[g_environ_len]; std::memset(g_os_env, 0, g_environ_len); /* 将原来存放环境变量内存的数据拷贝到新的内存数据中 */ char * copy_tmp_buffer = g_os_env; for (int index = 0; environ[index] != nullptr; ++index) { std::strcpy(copy_tmp_buffer, environ[index]); /* 同时将原来内存所指向的地址都指向新的内存地址 */ environ[index] = copy_tmp_buffer; copy_tmp_buffer += strlen(environ[index]) + 1; } }
-
经过上边的步骤现在已经有了一块很大的内存了,是原始的
程序启动命令长度
和系统环境变量长度
的内存的总长度。这两块内存均可以用来设置标题名称,但仍旧有限因此也不可设置特别长的名称。extern char** g_os_argv; // 命令行参数数组内存起始位置 extern int g_argv_len; // 命令行参数数组长度 void set_proc_title(const char * _title) { // g_os_argv = argv; 在 main函数中将参数数组指给全局指针 /* argv 和 environ 内存总和 */ size_t pro_param_len = g_argv_len + g_environ_len; /* 标题长度 */ size_t title_len = strlen(_title); if(pro_param_len <= title_len) { /* 标题过长了 */ return; } /* 原始参数全部不要了 */ g_os_argv[1] = nullptr; /* 由于 environ 地址已经指向了新的内存,因此 argv 和 environ 的内存完全是用来设置标题 */ /* 这里将全部内存都初始化了 */ std::memset(g_os_argv[0], 0, pro_param_len); /* 写入标题字符串 */ strcpy(g_os_argv[0], _title); }
另外需要注意,修改标题后原来的名称和辅助参数均不可用了,如果需要辅助参数则需要在修改之前将参数获取并留存。
主/子进程设置标题示例
有了前边的准备工作就可以区分主进程和子进程了,示例程序如下:
extern char** g_os_argv; // 命令行参数数组内存起始位置
extern int g_argv_len; // 命令行参数数组长度
extern char* g_os_env; // 系统环境变量数组内存起始位置-新
extern int g_environ_len; // 系统环境变量数组长度
int start_work_process() {
pid_t pid = fork();
switch (pid)
{
case -1: //产生子进程失败
return -1;
case 0: //子进程分支
set_proc_title("work process");
while(true) {
// 循环执行子进程业务
}
break;
default:
break;
}
return pid;
}
int main(int argc, char * argv[]) {
/* 计算程序变量的长度 */
for (int index = 0; index < argc; ++index) {
g_argv_len += std::strlen(argv[index]) + 1;
}
/* 环境变量长度 */
for (int index = 0; environ[index] != nullptr; ++index) {
g_environ_len += std::strlen(environ[index]) + 1;
}
g_os_argv = argv;
/* 拷贝系统环境变量 */
copy_env_buffer();
set_proc_title("master process");
start_work_process();
while(true) {
// 循环执行主进程业务
}
}
~$ ps -eo pid,ppid,sid,tty,pgrp,args,stat | grep -E 'bash|PID|process'
PID PPID SID TT PGRP COMMAND STAT
1877 1876 1877 pts/0 1877 -bash Ss
1893 1877 1877 pts/0 1893 master process S+
1894 1893 1877 pts/0 1893 work process S+