fork、exec 踩坑记录

在 linux 中,使用 fork 创建的子进程,子进程共享着父进程的资源,这些资源包括内存,信号,打开的文件等。就如同刚出生的小孩,和父母共享着家里的房子,存款等。

(1)内存不需要我们特别关心,因为系统的写时拷贝保证了,在写的时候,父子进程的内存是分离的。

(2)信号需要我们注意,在 fork 之后,子进程和父进程的共享着信号处理函数,如果不想共享,那么可以使用 signal 来修改信号的行为。如果在 fork 之后,通过 exec 来加载子进程,那么即使没有使用 signal 来修改信号的行为,在 exec 之后,信号也会被改为默认处理方式。

(3)打开的文件需要我们注意,即使在 exec 之后,子进程还是和父进程共享着打开的文件。

1 内存

在讨论父子进程共享的资源时,内存是最常被讨论的。内存使用写时拷贝技术,也就是说刚 fork 的子进程和父进程共享着父进程的内存空间,直到内存被写的时候,才会申请一块新的内存,这个时候子进程和父进程才会 "分家"。

下边这篇博客通过一个实例展示了内存的写时拷贝:

实例观察 linux 内存懒加载 和 写时拷贝_linux bbs段 内存懒加载-CSDN博客

2 信号

fork 出来的子进程,共享者父进程的信号处理函数。在实际应用中,如果不加处理,本来发送给子进程的信号,因为子进程还在共享者父进程的信号处理函数,最终会调用到父进程的信号处理函数,可能会导致问题。

如下代码,父进程中使用 signal() 注册了 SIGTERM 的信号处理函数。然后使用 fork() 创建了一个子进程。分别打印出来父进程和子进程的 pid。父进程睡眠 2s 之后向子进程发送 SIGTERM 信号,发给子进程的信号也会被 signalHandler() 处理。也就是说,虽然子进程中没有显式的注册 SIGTERM 的信号处理函数,因为子进程共享者父进程的信号处理函数,所以发给子进程的信号也会被父进程注册的信号处理函数处理。

#include <iostream>
#include <csignal>
#include <unistd.h>

using namespace std;

void signalHandler(int signum) {
    cout << "signal "<< signum << ", pid: " << getpid() << std::endl;
}

int main() {
    signal(SIGTERM, signalHandler);

    cout << "parent pid: " << getpid() << std::endl;
    pid_t pid = fork();
    if (0 == pid) {
      pid_t pid1 = getpid();
      cout << "child pid: " << pid1 << std::endl;
      kill(pid1, SIGSTOP);
      sleep(100);
    } else {
      sleep(2);
      kill(pid, SIGTERM);
      while (true) {
        sleep(1);
      }
    }
    return 0;
}

如果不想让子进程共享父进程的信号处理函数,那么可以在子进程一开始的时候,将信号设置为默认处理方式 signal(SIGTERM, SIG_DFL),如下所示。

    if (0 == pid) {
      signal(SIGTERM, SIG_DFL);
      cout << "child pid: " << getpid() << std::endl;
      while(1);
    } else {
      sleep(2);
      kill(pid, SIGTERM);
      while (true) {
        sleep(1);
      }
    }

2.1 exec 也可以覆盖父进程的信号处理函数

在实际使用中,子进程往往是通过 exec 系统调用加载一个可执行文件来启动的。即使在 fork 之后没有使用 signal(SIGTERM, SIG_DFL) 来修改信号处理函数,在调用 exec 之后,也会将信号处理函数改成默认的。

如下代码,在 fork() 之后,使用 exec() 来加载子进程。

#include <iostream>
#include <csignal>
#include <unistd.h>

using namespace std;

void signalHandler(int signum) {
    cout << "signal "<< signum << ", pid: " << getpid() << std::endl;
}

int main() {
    signal(SIGTERM, signalHandler);

    cout << "parent pid: " << getpid() << std::endl;
    pid_t pid = fork();
    if (0 == pid) {
      cout << "child pid: " << getpid() << std::endl;
      execv("./a.out", NULL);
    } else {
      sleep(2);
      kill(pid, SIGTERM);
      while (true) {
        sleep(1);
      }
    }
    return 0;
}

3 打开的文件

如下代码,父进程创建了一个 tcp 监听套接字,然后使用 fork 创建了子进程,创建子进程之后,父进程就关闭了监听套接字。

可以查看 /proc/[子进程 pid]/fd,可以看到 socket 在子进程还是存在的。

#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <iostream>

using namespace std;

int CreateTcpServer() {
    int listen_fd = -1;

    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("192.168.74.130");
    server_addr.sin_port = htons(12345);

    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("bind error.\n");
        return -1;
    }

    if (listen(listen_fd, 16) < 0) {
        printf("listen error.\n");
        return -1;
    }
    return listen_fd;
}

int main() {
    int listen_fd = CreateTcpServer();
    cout << "tcp listen fd: " << listen_fd << std::endl;
    cout << "parent pid: " << getpid() << std::endl;
    pid_t pid = fork();
    if (0 == pid) {
      cout << "child pid: " << getpid() << std::endl;
      execv("./a.out", NULL);
    } else {
      close(listen_fd);
      while (true) {
        sleep(1);
      }
    }
    return 0;
}

2575 是父进程 pid,2576 是子进程 pid,可以看到父进程 close(listen_fd) 之后,父进程中已经不存在这个 fd 了,但是子进程中还是存在的。

这种情况会导致父进程再次启动的时候启动失败。为了避免这种情况可能导致的问题,可以在 fork 之后,子进程 exec 之前,把不需要共享的 fd 关闭。

3.1 close-on-exec

除了上边说的,在 fork() 之后,exec() 之前,将子进程不需要的 fd 关闭之外,还有另外一种方式可以在调用 exec 之后关闭不需要共享的 fd。

close-on-exec,可以使用 fcntl() 给 fd 设置这个标志,这个标志的意思就是当 exec() 的时候关闭。如下代码,在 main() 中创建了两个 fd: fd1 和 fd2,将 fd2 设置 FD_CLOEXEC 标志。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <iostream>

using namespace std;

int CreateTcpServer(short port) {
    int listen_fd = -1;

    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("192.168.74.130");
    server_addr.sin_port = htons(port);

    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("bind error.\n");
        return -1;
    }

    if (listen(listen_fd, 16) < 0) {
        printf("listen error.\n");
        return -1;
    }
    return listen_fd;
}

#include <fcntl.h>
#include <unistd.h>

int set_fd_cloexec(int fd) {
    int flags = fcntl(fd, F_GETFD);
    if (flags == -1) {
        return -1;
    }

    flags |= FD_CLOEXEC;
    if (fcntl(fd, F_SETFD, flags) == -1) {
        return -1;
    }

    return 0;
}

int main() {
    int fd1 = CreateTcpServer(1111);
    int fd2 = CreateTcpServer(22222);
    cout << "fd1 = " << fd1 << ", fd2 = " << fd2 << std::endl;
    if (set_fd_cloexec(fd2) == -1) {
      std::cout << "set fd cloexec failed\n";
    }

    cout << "parent pid: " << getpid() << std::endl;
    pid_t pid = fork();
    if (0 == pid) {
      cout << "child pid: " << getpid() << std::endl;
      execve("./a.out", NULL, NULL);
    } else {
      while (true) {
        sleep(1);
      }
    }
    return 0;
}

fd1 是 3, fd2 是 4。父进程 id 是 2624,子进程 id 是 2625。

查看 proc 下,两个进程打开的文件,可以看到父进程中 3 和 4 都是打开的,子进程中只有 3 是打开的。

 

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值