fork生成子进程与执行exec子进程的区别

背景

项目中想使用多进程的模式。一个控制进程,加上N个工作进程。即是master+worker进程的模式,与nginx的进程模式类似的思路。master创建并管理worker进程,而且他们之间需要能够进行通信。设计思路如下图所示:
fork生成子进程与执行exec子进程的区别_第1张图片

遇到问题

如何创建woker进程

linux系统下,创建进程,肯定使用fork来做系统调用。然后fork之后,我们通过ps看到子进程名称也是master,从使用角度来看,并不是很舒服。

子进程如何修改进程名称为worker

方案一:
使用prctl(PR_SET_NAME, “process_name”, NULL, NULL, NULL); 但自己尝试多次未能修改成功,具体原因未找到,待后续研究 。

方案二:
使用exec函数,用新的可执行文件替换当前进程。测试可行。

master如何与worker通信

socketpair通信方式

对于父子进程,如果需要双向通信的话,可以考虑管道或者socket。
1、管道因为父子进程只能单向通信,需要开两条管道进行双向通信,比较麻烦。
2、可以使用socketpair来创建一对通信socket。fork之后,因为子进程继承了父进程的文件描述符。可参考如下示例代码进行创建socketpair通信:

#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include  
#include  
const char* str = "SOCKET PAIR TEST\n";
int main(int argc, char** argv) {
    bool is_worker = false;
    if (argc == 2) {
        printf("child process here, pid[%d]\n", getpid());
        is_worker = true;
    }   

    char buf[128] = {0};
    pid_t pid; 


    int socket_pair[2];
    if (is_worker) {
        printf("socket_pair[0] is [%d], socket_pair[1] is [%d]\n", 
            socket_pair[0], socket_pair[1]);

        printf("try to hard code socket[0] is 3\n");
        read(3, buf, sizeof(buf));    
        printf("Read result: %s, pid: %d\n",buf, getpid());
        for (;;) {
            printf("sleep for debug\n");
            sleep(1);
        }   
    }   

    if(socketpair(AF_UNIX, SOCK_DGRAM, 0, socket_pair) == -1 ) { 
        printf("Error, socketpair create failed, errno(%d): %s\n", errno, strerror(errno));
        return -1; 
    }   

    printf("parent socket_pair[0] is [%d], socket_pair[1] is [%d]\n", 
            socket_pair[0], socket_pair[1]);
    pid = fork();
    if(pid < 0) {
        printf("Error, fork failed, errno(%d): %s\n", errno, strerror(errno));
        return -1; 
    } else if(pid > 0) {
        //关闭另外一个套接字
        close(socket_pair[0]);
        int size = write(socket_pair[1], str, strlen(str));
        printf("Write success, pid: %d\n", getpid());

    } else if(pid == 0) {
        //关闭另外一个套接字
        close(socket_pair[1]);
        read(socket_pair[0], buf, sizeof(buf));
        printf("Read result: %s, pid: %d\n",buf, getpid());
    }

    for(;;) {
        sleep(1);
    }

    return 0;
}

socketpair如果遇上exec

根据上面的分析,如果子进程中使用了exec,那么,子进程中的代码段,数据段则是与父进程无关的数据,无法知道父进程中的socketpair的值,那此时还能使用socketpair进行父子进程通信么?

学习nginx源码,此路可行

猜想,既然是父子进程,虽然子进程执行了exec函数,有了全新的进程代码段与数据段,但文件描述符是不是也应该是继续过来了。写了一个单独的main函数编成bin来执行exec,通过在父进程中把子进程需要的fd打印出来,在子进程在直接对这个fd进行读数据。
结果:可以读出数据来。
那证明子进程虽然是执行了exec操作,有了自己全新的代码段与数据段,但还是继承了父进程打开的文件表项。否则父子进程就与普通的陌生进程没有什么区别了。

验证猜想

我使用master进程fork之后,创建了worker进程,通过查看worker打开的文件描述符,发现确实worker进程创建之初就打开了四个文件描述符。虽然子进程执行了exec,无法通过共享数据的方式获取文件描述符的值,但可以通过传参的方式,把文件描述符通过exec函数传给子进程。
验证代码:

//把应用程序名称编成master
//g++ -o master master.cpp
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include  
#include  
const char* str = "SOCKET PAIR TEST\n";
int main(int argc, char** argv) {
    bool is_worker = false;
    if (argc == 2) {
        printf("child process here, pid[%d]\n", getpid());
        is_worker = true;
    }   

    char buf[128] = {0};
    pid_t pid; 


    int socket_pair[2];
    if (is_worker) {
        printf("socket_pair[0] is [%d], socket_pair[1] is [%d]\n", 
            socket_pair[0], socket_pair[1]);

        printf("try to hard code socket[0] is 3\n");
        read(3, buf, sizeof(buf));    
        printf("Read result: %s, pid: %d\n",buf, getpid());
        for (;;) {
            printf("sleep for debug\n");
            sleep(1);
        }   
    }   

    if(socketpair(AF_UNIX, SOCK_DGRAM, 0, socket_pair) == -1 ) { 
        printf("Error, socketpair create failed, errno(%d): %s\n", errno, strerror(errno));
        return -1; 
    }   

    printf("parent socket_pair[0] is [%d], socket_pair[1] is [%d]\n", 
            socket_pair[0], socket_pair[1]);

    pid = fork();
    if(pid < 0) {
        printf("Error, fork failed, errno(%d): %s\n", errno, strerror(errno));
        return -1; 
    } else if(pid > 0) {
        //关闭另外一个套接字
        close(socket_pair[0]);
        int size = write(socket_pair[1], str, strlen(str));
        printf("Write success, pid: %d\n", getpid());

    } else if(pid == 0) {
        //关闭另外一个套接字
        close(socket_pair[1]);
        //read(socket_pair[0], buf, sizeof(buf));        
        printf("Read result: %s, pid: %d\n",buf, getpid());
        char* new_argv[] = { (char*)"./worker", (char*)"hello", NULL};
        char* new_env[] = { NULL };
        if (-1 == execve("./master", new_argv, new_env)) {
            int err = errno;
            printf("create worker error, err[%d], err_msg[%s]\n",
                err, strerror(err));
            return -1;
        }
    }

    for(;;) {
        sleep(1);
    }

    return 0;
}    

父进程中创建的socketpair的值分别为3,4。在父进程中把3关闭,在子进程中,把4关闭。
fork生成子进程与执行exec子进程的区别_第2张图片
父进程号为20870,查看父进程打开的文件描述符,如下图所示:
fork生成子进程与执行exec子进程的区别_第3张图片
子进程号为20871,查看其文件描述符:
fork生成子进程与执行exec子进程的区别_第4张图片
可以看到,子进程执行exec之后,把父进程的文件描述符继承过来了。

fork+exec,父子进程继承了什么

最直接而且权威的做法,还是查看操作系统手机。man execve发现:

By default, file descriptors remain open across an execve(). File descriptors that are marked close-on-exec are closed; see the description of FD_CLOEXEC in fcntl(2). (If a file descriptor is closed, this will cause
the release of all record locks obtained on the underlying file by this process. See fcntl(2) for details.)

具体大家可以自行查看。
调用exec的子进程,他从父亲继承了什么?
1、execve只有当前的线程会复制到子进程。
2、Mutexes, condition variables, and other pthreads objects are not preserved。(与fork的子进程的区别是,mutex等锁资源不会被复制到exec的子进程中,而直接fork的子进程是会把mutex等资源继续到子进程中
3、文件描述符会被保留到子进程。
4、进程ID和父进程ID
5、当时工作目录与根目录。
6、文件锁。
7、进程信号屏蔽。
8、资源限制。

以上参考man execve与《unix环境高级编程》。

你可能感兴趣的:(C/C++,linux)