​最近遇到一个神奇的fork问题,坑了我2天半的时间,最后在另一个小伙伴的帮助下,找到问题根源,然后修改。此时,对于前人说的,fork的坑,也终于有点认识了。

基本的软件图如下:

主进程A收到云端B的命令,fork出子进程A1、A2、….、An,然后执行execv函数,打开新的可执行文件。Execv执行完成后,子进程Ai就拥有了和主进程A不同的镜像文件,这是Linux下创建新进程的典型方式。

主进程A收到云端的控制信令后,通过socket与子进程Ai通信:控制Ai,收集Ai的信息等(任意的子进程统称为子进程Ai,下同)。

主进程A有保活进程:如果A因为异常情况终止,会由保活进程自动拉起。

子进程Ai和主进程A之间有心跳机制及断线重连机制,因此子进程Ai会一直尝试连接主进程A,然后发送心跳报文。如果主进程A在指定时间内,没有收到子进程Ai的心跳报文,就标记子进程Ai下线,并反馈状态给云端。

上述是背景信息,问题出现在主进程A被主动Kill了,被保活进程拉起后,云端却显示很多子进程处于离线状态。

通过ps命令,发现子进程都在,并没有出异常。子进程Ai的日志,也显示心跳报文发送正常。通过GDB挂载离线的子进程Ai,发现其套接字状态也OK,心跳交互机制也正常。通过抓包,离线的子进程Ai,还在正常发送心跳报文,并且目的地、端口都没有问题,滑动窗口都正常。

奇怪的是,主进程A就是没有收到离线的子进程Ai的心跳报文,主进程A的日志也证实了这一点。

通过GDB挂载到主进程A中,发现其维护的相应的数据结构确实没问题。

一切都正常,但是结果却不对,太神奇了。

每次遇到这么的问题,我都说,肯定是个傻逼问题。

我开始怀疑主进程A的网络模型了。

主进程A用的是epoll网络模型,代码也是典型的《Unix网络编程》中的示例代码,确实没有什么问题。在accept那边进行打印,打印的次数,和在线的子进程Ai数相同,也即,离线的子进程Ai,根本就没有连接到主进程A上!

但是,抓包显示,离线的子进程Ai一直在发送心跳报文,并且端口确实是主进程的监听端口!

那么问题来了,到底是哪个进程在主进程被Kill期间,重新绑定了监听端口,进而收走了子进程Ai的报文?

太神奇了,太有意思了。

通过netstat查看网络端口情况,发现主进程的监听端口上的链接,并不是只有主进程拥有,子进程也有;通过lsof查看主、子进程打开的文件描述符信息,发现socket有点不对劲。子进程拥有的fd,超过了其自己打开的fd:这里包括socket,包括epoll。

于是在子进程的bind、listen、accept函数附近加日志,但并没有异常。

继续分析netstat、lsof的输出信息,突然间,小伙伴发现了问题所在:子进程是通过fork函数创建的,而fork出的子进程,默认是继承了父进程的句柄。

Fork手册中很清楚的写着:

The child inherits copies of the parent's set of open file descriptors.  Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent.  This means that the two descriptors share open file status  flags,  current file offset, and signal-driven I/O attributes (see the description of F_SETOWN and F_SETSIG in fcntl(2)).

问题的根源在于,主进程A通过fork创建了子进程Ai,子进程Ai继承了主进程A的fd:包括监听套接字、epoll句柄、accept的套接字等。当主进程A被手动Kill时,这些句柄,对于子进程Ai依然有效。

子进程定期发心跳,监听套接字有效、epoll句柄有效、accept的套接字也有效,因此,某些子进程就扮演了主进程的角色:收走了其他子进程发往主进程的报文,这也正是抓包显示的报文都正常的原因。

当主进程A重新被拉起,重新绑定监听端口,子进程发出去的报文,也可能被其收到。被主进程A收到报文的子进程,状态就是在线,反之就是离线。

于是,排查网络操作相关的函数,在创建监听套接字时增加SOCK_CLOEXEC标志;将epoll_create修改为epoll_create1,参数设置为EPOLL_CLOEXEC;将accept修改为accept4,标志设置为SOCK_CLOEXEC。这些都是为了不让子进程继承主进程的相关句柄。

SOCK_CLOEXEC的说明如下:

Set  the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.

O_CLOEXEC的说明如下:

O_CLOEXEC (Since Linux 2.6.23)

Enable the close-on-exec flag for the new file descriptor.  Specifying this flag permits a program  to  avoid  additional  fcntl(2)  F_SETFD  operations  to  set  the FD_CLOEXEC flag.  Additionally, use of this flag is essential in some multithreaded programs since using a separate fcntl(2) F_SETFD operation to set the FD_CLOEXEC flag does not suffice to avoid race conditions where one thread opens a file descriptor  at   the same time as another thread does a fork(2) plus execve(2).

epoll_create和epoll_create1的说明如下

int epoll_create(int size);

int epoll_create1(int flags);

DESCRIPTION

epoll_create() creates an epoll(7) instance.  Since Linux 2.6.8, the size argument is ignored, but must be greater than zero; see NOTES below.

epoll_create()  returns  a  file  descriptor referring to the new epoll instance.  This file descriptor is used for all the subsequent calls to the epoll interface.  When no longer required, the file descriptor returned by epoll_create() should be  closed  by  using  close(2).   When  all  file  descriptors referring to an epoll instance have been closed, the kernel destroys the instance and releases the associated resources for reuse.

epoll_create1()

If  flags is 0, then, other than the fact that the obsolete size argument is dropped, epoll_create1() is the same as epoll_create().  The following value can be included in flags to obtain different behavior:

EPOLL_CLOEXEC

Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the O_CLOEXEC flag in open(2)  for  reasons  why  this may be useful.

accept和accept4的说明如下:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

#define _GNU_SOURCE   /* See feature_test_macros(7) */

#include <sys/socket.h>

int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

SOCK_CLOEXEC    Set  the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.

修改完成后,测试了几次,问题解决了,很开心,确实长了见识。

但是,当项目发测时,进行问题单回归时,我又多测试了几次。然后,又被发现出问题:依然有子进程处于离线状态。

先抓包,报文正常;然后通过netstat以及lsof查看,发现处于离线状态的子进程,和主进程A连接的套接字很特殊:本地端口和对端端口相同。也即,如果主进程A监听的是20000端口,这个子进程向20000端口连接的socket的本地端口也是20000。在这个情形下,主进程A被Kill了,由于这个套接字既监听了写,也监听了读,因此,它发出去的报文被自己给接收了,真是神奇(操作系统通过端口找进程)。操作系统的套接字管理机制,真是有意思,后续可以重点研究下。

这个现象的根源在于,端口复用,将主进程A的监听端口设置为不可复用即可。当然,这里和fork的文件描述符继承无关,只是验证问题的一个小插曲。

这次的问题定位,真是一波三折,也真是有意思,只能说明自己弱鸡,需要学习的还很多。

fork的坑:文件描述符继承相关推荐

  1. Linux内核机制总结内存管理之用户页错误文件描述符(二十八)

    文章目录 1 用户页错误文件描述符 1.1 使用方法 1.2 技术原理 重要:本系列文章内容摘自<Linux内核深度解析>基于ARM64架构的Linux4.x内核一书,作者余华兵.系列文章 ...

  2. fork()子进程与父进程之间的文件描述符问题

    在C程序中,文件由文件指针或者文件描述符表示.ISO C的标准I/0库函数(fopen, fclose, fread, fwrite, fscanf, fprintf等)使用文件指针,UNIX的I/O ...

  3. FORK()子进程对父进程打开的文件描述符的处理

    总的来说,子进程将复制父亲进程的数据段,BSS段,代码段,堆空间,栈空间和文件描述符.而对于文件技术符关联内核文件表项(即STRUCT FILE结构),则是采取了共享的方式. 下面代码说明. I值分离 ...

  4. linux进程文件描述符 vnode,从flock引发的一个bug谈起(1) 进程的文件描述符

    引子 前两天我们QA发现了一个比较有意思的bug,我细细分析一下,发现多个进程卡死在一个·配置文件上.简单的说,我们为了防止多个进程同时写同一个配置文件,将文件格式破坏,我们用了flock,对于写打开 ...

  5. linux文件描述符与标识符,文件描述符fd

    这里以问答的方式来讨论这个问题: 1. 文件描述符 fd 和文件指针 FILE *的关系? 文件描述符是什么?我们知道每一个进程都有一个自己的PCB(进程控制块),进程控制块的结构是: struct ...

  6. Linux文件,文件描述符以及dup()和dup2()

    一.Linux中文件 可以分为4种:普通文件.目录文件.链接文件和设备文件. 1.普通文件 是用户日常使用最多的文件,包括文本文件.shell脚本.二进制的可执行和各种类型的数据. ls -lh 来查 ...

  7. [转] linux系统文件流、文件描述符与进程间关系详解

    http://blog.sina.com.cn/s/blog_67b74aea01018ycx.html linux(unix)进程与文件的关系错综复杂,本教程试图详细的阐述这个问题. 包括:     ...

  8. UNIX中文件描述符和文件指针

    文件描述符 在C程序中,文件由文件指针或者文件描述符表示.ISO C的标准I/0库函数(fopen, fclose, fread, fwrite, fscanf, fprintf等)使用文件指针,UN ...

  9. linux存储--文件描述符以及file结构体(一)

    一.什么是文件描述符 在Linux下一切皆文件,对于内核而言,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数,当打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符.当读. ...

最新文章

  1. 配置apache2目录
  2. hive与hbase整合方式和优劣
  3. spingmvc-参数传递
  4. 网狐棋牌(二) CQueueServiceEvent初步分析
  5. Windows7环境下用VirtualBox (5.1)上安装Ubuntu 17.10
  6. 前端构建新世代,Esbuild 原来还能这么玩!
  7. 前端学习(2667):退出编辑状态
  8. Node中使用token(基于第三方包jsonwebtoken)
  9. 论文浅尝 | 对于知识图谱嵌入表示的几何形状理解
  10. Gradle Issue: OutOfMemoryError: PermGen space
  11. 区块链中的密码学(五)-零知识证明简述
  12. ACM 学习笔记(四) 数据结构之树、二叉树、完全二叉树、二叉查找树、AVL树、红黑树、B树、B+树
  13. windows service 2008 R2 安装net4.6环境失败,windows service 2008 R2 升级sp1问题
  14. Spring学习笔记17--在XML中使用SPEL
  15. vs中四点画矩形的算法_实战基于图割算法的木材表面缺陷图像分析
  16. Caliburn.Micro框架学习资料积累
  17. java 操作日志记录_高效日志系统搭建秘技!架构师必读
  18. win7下好用的虚拟光驱,免安装,体积小
  19. 线性代数学习笔记——第十九讲——克拉默法则
  20. GPRS DTU工作原理 GPRS DTU通信终端

热门文章

  1. readlink() 函数
  2. Fiddler抓包手机连不上网
  3. 微信小程序表单提交没反应时应该检查focus或autofocus属性
  4. 对比下HTML5和小程序的组件标签的区别
  5. 使用驱动器中的光盘之前需进行格式化--resolution
  6. 找保姆APP开发有哪些需要深入思考的地方?
  7. Idea使用又Get新技能
  8. 加入安全服务行业,做一名新时代的网络安全守护者
  9. PHPExcel 换行符
  10. 在微信实现唯一身份填报