此问题来自项目上,应用程序本身由它的父进程启动,父进程监听SIGCHLD
信号,即子进程退出时,父进程会收到这个信号,然后立即通过execlp
重新启动子进程,确保子进程异常崩溃会被重新拉起来。而子进程(我们实际的业务应用)也会在某些地方fork新的进程,干别的事情。
出现的问题是,进程被重新拉起来后,一个socket的bind动作失败,错误为bind: Address already in use
。netstat查看,发现是crond占用了这个端口。最开始觉得比较奇怪,crond按道理不会使用socket,更不可能恰好绑定这个端口。并且还发现crond进程的/proc/$(pidof crond)/fd居然打开了显卡设备节点,这个就完全不可能了。打开显卡的行为是我们的应用程序,这两者有什么关联呢?查看代码发现,我们的应用会fork子进程,然后执行shell命令/etc/init.d/crond restart
。经同事提醒,子进程会继承父进程打开的文件描述符!原来问题在这里,几年前看APUE(Unix环境高级编程)时,确实记得这一点,太久没搞忘记了。第8章 <进程控制>提到的这点。
为了加深映像,模拟测试验证一下:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
int main()
{
int fd;
pid_t pid;
struct sockaddr_in addr;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0){
perror("socket");
return -1;
}
memset(&addr, 0, sizeof(struct sockaddr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(4567);
if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0){
perror("bind");
close(fd);
return -1;
}
if (listen(fd, 5) < 0){
perror("listen");
close(fd);
return -1;
}
pid = fork();
if (pid == 0){
printf("I am child\n");
while (1)
{
sleep(1);
}
}else if (pid > 0){
printf("I am parent\n");
close(fd);
return 0;
}else{
perror("fork");
close(fd);
return -1;
}
close(fd);
return 0;
}
上面代码父进程中bind 4567端口,然后fork后,父进程退出,子进程继续运行,此时子进程成为孤儿进程,由1号进程托管,在ubuntu20.04上是由systemd托管。先查看成为孤儿进程的子进程打开的文件描述符:
ls /proc/$(pidof ctest)/fd -l
total 0
lrwx------ 1 a a 64 Aug 18 18:02 0 -> '/dev/pts/2 (deleted)'
lrwx------ 1 a a 64 Aug 18 18:02 1 -> '/dev/pts/2 (deleted)'
lrwx------ 1 a a 64 Aug 18 18:02 2 -> '/dev/pts/4 (deleted)'
lrwx------ 1 a a 64 Aug 18 18:02 3 -> 'socket:[28406147]'
netstat -antp | grep 4567
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:4567 0.0.0.0:* LISTEN 3535349/ctest
发现子进程确实继承了父进程打开的文描述符,并且端口的占用也继承了。再次启动程序
./ctest
bind: Address already in use
问题复现。如何解决这个问题呢?
man socket
可知,socket的第二个参数type,可以通过OR的形式指定bit标识,具体参数为SOCK_CLOEXEC
,它表示socket创建的fd在exec时,做close动作。即代码改为:
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
编译重新验证,先杀掉最开始的成为孤儿进程的子进程。重复验证过程,问题确认得到解决。
- 以此类推,如果不是socket,是其他类型的东西,例如文件,设备节点等。则可以在open时,指定flags:
O_CLOEXEC
,或者对fd进行fcntl操作
open(path, O_RDWR | O_CLOEXEC)
或者开时不指定,后续通过fcntl更改flags
int flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
-
还有一种情况,父进程调用第三方库,第三方库未指定
O_CLOEXEC
标识,而我们又不想子进程继承打开的描述符,避免误操作到,引发不必要的麻烦,此时可以通过clone
方式,而不是fork
来创建子进程,clone
可以指定标志,选择继承父进程的哪些东西,例如CLONE_FILES
控制是否继承父进程打开的文件描述符,我们这里可以选择不继承。 -
手动关闭文件描述符,fork和exec之间是允许我们做自己想做的事情,例如在这里,我们关闭所有文件描述符,一个典型的参考例子时AUEP中守护进程里面的例子,先获得进程最大的文件描述符编号,然后逐个close。
struct rlimit rl;
getrlimit(RLIMIT_NOFILE, &rl);
for(i=0;i<rl.rlim_max; i++)
{
close(i);
}
标签:socket,bind,描述符,fd,18,进程,addr
From: https://www.cnblogs.com/thammer/p/17640540.html