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_FILENOSTDOUT_FILENOSTDERR_FILENO,这三个文件描述符分别对应了012三个数字,再打开其它设备或文件均大于这三个数,因此,最后一步是将文件描述符大于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

多进程的区分

当一个主进程启动了多个子进程之后,无论是主进程还是子进程在psCOMMAND列显示的都是同一个名称

# 注意这里需要将 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+

这种情况下除了分析PIDPPID的关系外,基本无法区别哪个是子进程哪个是父进程。这种情况下可以修改主进程和子进程的名称加以区分。

名称由来

在上面使用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

0x7ffc32a8782f0x7ffc32a8783b之间正好相差./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+
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值