在 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 是打开的。