Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3426045
  • 博文数量: 198
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 7246
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

将晦涩难懂的技术讲的通俗易懂

文章分类

全部博文(198)

文章存档

2023年(9)

2022年(4)

2021年(12)

2020年(8)

2019年(18)

2018年(19)

2017年(9)

2016年(26)

2015年(18)

2014年(54)

2013年(20)

分类: WINDOWS

2014-12-28 13:30:08


tcp服务器端fork子进程没有exit引起的问题及原因
——lvyilong316
在编写tcp服务端使select时,编写了如下代码:



点击(此处)折叠或打开

  1. for( ; ; )
  2. {
  3.  FD_SET(listenfd,&rset);
  4.  n=select(listenfd+1,&rset,NULL,NULL,NULL);
  5.  if(FD_ISSET(listenfd,&rset))
  6.  {
  7.     clilen=sizeof(cliaddr);
  8.     connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
  9.     if( (childpid=fork())==0 )
  10.    {
  11.    close(listenfd);
  12.    printf("a child is forked\n");
  13.    //exit(0);
  14.    }
  15.   close(connfd);
  16.  }
  17. }


子进程忘记了退出,那么当有客户发起链接时,服务端出现了以下情况:

当时觉得很奇怪,按理说就是子进程没有退出也应该和父进程一样阻塞在select上啊,为什么会出现死循环。又来检查了select的返回值,如下:


点击(此处)折叠或打开

  1. n=select(listenfd+1,&rset,NULL,NULL,NULL);
  2.     if(n==-1)
  3. perror("select error");


从新启动服务端,当有客户链接时,服务端显示如下:

这下我们知道其中的原因了,因为监听套接字描述符在子进程中已经close,所以对一个close的描述符select会直接返回错误,从而子进程不会阻塞在select,造成死循环。

我们也可以如下验证,在子进程中不close监听套接字:


点击(此处)折叠或打开

  1. for( ; ; )
  2. {
  3.   FD_SET(listenfd,&rset);
  4.   n=select(listenfd+1,&rset,NULL,NULL,NULL);
  5.         if(n==-1)
  6.   perror("select error");
  7.   if(FD_ISSET(listenfd,&rset))
  8.   {
  9.     clilen=sizeof(cliaddr);
  10.     connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
  11.     if( (childpid=fork())==0 )
  12.    {
  13.    //close(listenfd);
  14.    printf("a child is forked\n");
  15.    //exit(0);
  16.    }
  17.    close(connfd);
  18.   }
  19. }


再次运行服务端,并从客户端发起链接,服务度显示如下:

我们发现这次子进程阻塞在了select,因为被select的套接字没有close

但是这里面有个问题,有人会问父进程也打开了监听套接字,fork后监听套接字的打开引用计数会变为2,子进程close只会使监听套接字的打开引用计数减1,并不会真正关闭套接字,那为什么子进程就不能select呢?

这个问题要从VFS角度说明,即父子进程描述符、打开文件表、FILE(socket)对象之间的关系。

可以看到,父子进程有各自的打开文件表(files_struct),但是共享底层的FILE对象,而close的效果就是使FILE对象的引用计数减一,也就是解除了一个描述符和FILE对象的关联。对于子进程,当调用close后,子进程中的listenfd对应的指针下标已经不再和对应监听套接字对象相关联了,所以从子进程角度来说这个listenfd已经是个无效的描述符,不能再对其进行操作了。

    

补充:以上select的例子同样适用于accept,当子进程close(listenfd)后没有exet退出,再次执行accept同样会造成Bad file descriptor。例如如下tcp服务端程序:


点击(此处)折叠或打开

  1. for(;;)
  2.     {
  3.         socklen_t addrlen = sizeof(client_addr);
  4.         //accept返回客户端套接字描述符
  5.         connfd = accept(listenfd,(struct sockaddr *)&client_addr,&addrlen);
  6.         if(connfd<0)
  7.            perror("accept error");
  8.         printf("receive connection\n");
  9.         if((pid = fork()) == 0) //子进程,与客户端通信
  10.         {
  11.             close(listenfd);
  12.             //exit(0);
  13.         }
  14.         else
  15.         {
  16.             close(connfd);
  17.         }
  18. }


当客户端连接后效果如下图所示。

阅读(6711) | 评论(3) | 转发(1) |
给主人留下些什么吧!~~

lvyilong3162017-07-23 15:04:10

lionwes:有疑问,《Linux网络编程》 这本书说是可以close(listenfd); 但是您这边说不行。。
详细请看《Linux网络编程》 里面的7.3.2 服务器网络编程

我的例子中,子进程没有退出。其实这是当时写的一个bug,只是对这个bug导致的想象比较奇怪,所以分析了一下。能不能close完全看你程序怎么写,这不是一个规定

回复 | 举报

lionwes2017-07-14 14:08:20

代码如下:
 /* 主循环过程 */
 for(;;) {
  int addrlen = sizeof(struct sockaddr);
  /* 接收客户端连接 */
  sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen);
  if(sc < 0){  /* 出错 */
   continue; /* 结束本次循环 */
  } 
  
  /* 建立一个新的进程处理到来的连接 */
  pid = fork();  /* 分叉进程 */
 

lionwes2017-07-14 14:06:53

有疑问,《Linux网络编程》 这本书说是可以close(listenfd); 但是您这边说不行。。
详细请看《Linux网络编程》 里面的7.3.2 服务器网络编程