5.慢系统调用及EINTR

还记得前面readn和writen函数么?里面有个EINTR,现在就来谈谈这个,这个很重要。

Linux世界有个叫信号的东西,感觉他就像一位隐士,很少遇到他,而他又无处不在。当你船到桥头时,他从天而降,将你领入另一片天地。(唉,博客再写下去我都可以改行了)前面已经初步窥探了信号的神奇,一个“小小”的SIGPIPE能让我们不知道怎么回事就惨遭“灭门”。那还有其他千千万万的信号呢,是不是也会对我们写的网络程序表现出神奇的现象。答案:有!还有很多!

写到这,才发现自己还没涉及网络编程涉及的那些函数,这也是一项基础知识的学习。在前面知识的基础上,相信很快能掌握那些套接字函数。因此,那些函数的说明也就不罗嗦了,说实话,即使你用错了也影响不大,编译器会帮你发现大部分问题(当然,最好不要有这种偷懒的表现)。继续谈细节。

我们可以发现,诸如:read、write、select、connect、accept这些函数都是阻塞的,阻塞是啥意思?就是说跑到他们,如果条件不合适,程序就只能死等了,下不去了(想到了下水道,呵呵)。

话说死等就死等呗,正好拿他作为条件符合的判断依据。没错,正常想法就是这样,可万一不是因为正常原因让他苏醒,那就不好了,睡着被吵醒脾气还是很大的。有人要笑了,怎么可能,下水道堵了只有等到下水管通了才行啊,这不是很简单的道理吗?可是,我想说,还有一种可能:下水管裂了!!!扯多了,我们看一下测试程序(以accept来做试验):

这次来高级一点的,搞个多进程,我很少用多进程,所以用错了不要笑我。服务器程序:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h> #define PORT 1234
#define BACKLOG 5
#define MAXDATASIZE 1000 void sig_chld(int signo)
{
pid_t pid;
int stat = ; pid = wait(&stat);
printf("child %d terminated(stat:%d)\n", pid, stat); return;
} void signal_ex(int signo, void* func)
{
struct sigaction act, oact; act.sa_handler = func;
sigemptyset(&act.sa_mask); //清空此信号集
act.sa_flags = ; if (sigaction(signo, &act, &oact) < )
{
printf("sig err!\n");
} //sigaction(SIGINT, &oact, NULL); //恢复成原始状态
return;
} int main()
{
int listenfd, connectfd;
struct sockaddr_in server;
struct sockaddr_in client;
socklen_t addrlen;
char szbuf[MAXDATASIZE + ] = {};
int num = ;
pid_t pid_child; if ((listenfd = socket(AF_INET, SOCK_STREAM, )) == -)
{
perror("Creating socket failed.");
exit();
} int opt = SO_REUSEADDR;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -)
{
perror("Bind()error.");
exit();
}
if (listen(listenfd, BACKLOG) == -)
{
perror("listen()error\n");
exit();
} signal_ex(SIGCHLD, sig_chld);
while ()
{
addrlen = sizeof(client);
printf("start accept!\n");
if ((connectfd = accept(listenfd, (struct sockaddr*)&client, &addrlen)) == -)
{
#if 0
if (EINTR == errno)
{
printf("EINTR!\n");
continue;
}
#endif perror("accept()error\n");
exit();
}
printf("You got a connection from cient's ip is %s, prot is %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port)); if ( == (pid_child = fork()))
{
close(connectfd);
close(listenfd);
printf("child a ha!\n");
sleep();
exit();
} close(connectfd);
connectfd = -;
} close(listenfd); return ;
}

server_eintr

客户端程序:

 #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <signal.h> #define PORT 1234
#define MAXDATASIZE 1000 int main(int argc, char *argv[])
{
int sockfd, num;
char szbuf[MAXDATASIZE] = {};
struct sockaddr_in server; if (argc != )
{
printf("Usage:%s <IP Address>\n", argv[]);
exit();
} signal(SIGPIPE, SIG_IGN); if ((sockfd=socket(AF_INET, SOCK_STREAM, )) == -)
{
printf("socket()error\n");
exit();
}
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = inet_addr(argv[]);
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -)
{
printf("connect()error\n");
exit();
} memset(szbuf, 'a', sizeof(szbuf));
while ()
{
printf("a send\n");
//send(sockfd, szbuf, sizeof(szbuf), 0);
sleep();
} close(sockfd); return ;
}

client_eintr

运行后,打印如下:

在刚开始写程序时,我们往往会很相信系统调用,我们总是会认为系统调用很可靠,基本不会失败。注意,只是基本,因为我们说这话的时候本身信心就不足。因此,很多人在调用系统调用后就不考虑返回值、不考虑出错处理了,这是多么危险的一种心态。当然,也有些人,本身对某个系统调用不了解,但他有很强的责任意识,他会认为系统调用一旦出错,那说明系统就出大问题了,该直接退出整个进程。后者当然是无可厚非的,起码主动退出往往比异常退出来得更“温柔”一些。

回到这个例子,很显然,子进程退出后,accept错误返回了,错误处理就是退出整个进程。(这个例子主要是给SIGCHLD设置了一个默认处理函数,子进程退出时,主进程将捕获这个信号)你可以说我刻意构造了这样一个错误,可是,对于一个很大的系统来说,任何行为都可能导致这种“刻意”。

我们回到书上寻找原因,Stevens提到,“我们用慢系统调用来描述那些可能永远堵塞的系统调用(函数调用)”、“适用于慢系统调用的基本规则是:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。”

我们按照这个思路,修改服务器代码:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h> #define PORT 1234
#define BACKLOG 5
#define MAXDATASIZE 1000 void sig_chld(int signo)
{
pid_t pid;
int stat = ; pid = wait(&stat);
printf("child %d terminated(stat:%d)\n", pid, stat); return;
} void signal_ex(int signo, void* func)
{
struct sigaction act, oact; act.sa_handler = func;
sigemptyset(&act.sa_mask); //清空此信号集
act.sa_flags = ; if (sigaction(signo, &act, &oact) < )
{
printf("sig err!\n");
} //sigaction(SIGINT, &oact, NULL); //恢复成原始状态
return;
} int main()
{
int listenfd, connectfd;
struct sockaddr_in server;
struct sockaddr_in client;
socklen_t addrlen;
char szbuf[MAXDATASIZE + ] = {};
int num = ;
pid_t pid_child; if ((listenfd = socket(AF_INET, SOCK_STREAM, )) == -)
{
perror("Creating socket failed.");
exit();
} int opt = SO_REUSEADDR;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -)
{
perror("Bind()error.");
exit();
}
if (listen(listenfd, BACKLOG) == -)
{
perror("listen()error\n");
exit();
} signal_ex(SIGCHLD, sig_chld);
while ()
{
addrlen = sizeof(client);
printf("start accept!\n");
if ((connectfd = accept(listenfd, (struct sockaddr*)&client, &addrlen)) == -)
{
#if 1
if (EINTR == errno)
{
printf("EINTR!\n");
continue;
}
#endif perror("accept()error\n");
exit();
}
printf("You got a connection from cient's ip is %s, prot is %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port)); if ( == (pid_child = fork()))
{
close(connectfd);
close(listenfd);
printf("child a ha!\n");
sleep();
exit();
} close(connectfd);
connectfd = -;
} close(listenfd); return ;
}

server_eintr

修改思路为:EINTR预示着内核中断了慢系统调用,当错误返回时,检查该错误,如果是EINTR,我们就重新启动被中断的系统调用。(有很多系统调用是不可重入的,那就不能简单地重启来解决问题了,这个要注意)下面是打印:

打印“EINTR”了,有没有,并且我重新打开客户端后,accept成功了,我们成功规避了EINTR带来的错误。同样,像read,write等其他慢系统调用也可以通过这一方式进行出错处理。这无疑使我们的程序更加健壮。这当然是必须的了。

不过,还有两个注意点:首先,在这个测试代码里,有些人会说我是不是傻啊,现成的signal不用,为啥要自己封装一个函数(用sigaction)?这里面确实是有原因的,有心的人可以测试一下,两者的结果是不同的,至于原因,可以自己去寻找一下。

另外,前面也提到,并不是所有系统调用都能重新启动的,网络编程里也有这样的家伙,像connect。试想一下,connect调用后就发起了TCP的三次握手,这个过程完全取决于网络和底层协议栈的响应了,这个过程一旦开始,就停不下来了,更谈不上再来一次,这点也是很容易想明白的。

那么,connect遇到EINTR错误该怎么办呢?很想这一节给出的。不过想要把这个问题说明白还需要构造测试代码,还是放到下一节吧。TCP编程方面的知识差不多只积累了这么多,相信对于那么厚的《UNIX网络编程》来说,只能算是冰山一角,后续再慢慢学习补充吧。不过,既然是“快速上手”,我也是要对得住标题的,在目前看过的网络程序代码,上述知识基本能成就一个完整、稳健的网络连接程序了(不考虑上层应用的复杂度)。在下一节给出connect的使用方法及完整的TCP客户端和服务器代码。

socket网络编程快速上手(二)——细节问题(4)的更多相关文章

  1. socket网络编程快速上手(二)——细节问题(5)(完结篇)

    6.Connect的使用方式 前面提到,connect发生EINTR错误时,是不能重新启动的.那怎么办呢,是关闭套接字还是直接退出进程呢?如果EINTR前,三次握手已经发起,我们当然希望链路就此已经建 ...

  2. socket网络编程快速上手(二)——细节问题(1)

    三.细节问题一个也不能少 Socket编程说简单也简单,程序很容易就能跑起来,说麻烦还真是麻烦,程序动不动就出问题.记得刚开始写网络代码的时候,那真是令人抓狂的经历,问题一个套一个,一会服务器起不来了 ...

  3. socket网络编程快速上手(一)

    工作以来,写了很多socket相关的代码.磕磕碰碰,走了很多弯路,也积累了一些东西,今天正好整理一下.为了证明不是从书上抄来的,逻辑会有点乱(借口,呵呵)!知识点的介绍也不会像书上说的那么详细和精准, ...

  4. socket网络编程快速上手(二)——细节问题(3)

    3.SIGPIPE问题 人怕牺牲,我们写的程序也一样,人有死不瞑目,程序又何尝不是?程序跑着跑着,突然就崩掉了.好一点的牺牲前告诉你些打印,差点的也能用core文件等一些手段查出死在哪了,最惨不忍睹的 ...

  5. socket网络编程快速上手(二)——细节问题(2)

    2.TCP数据包接收问题 对初学者来说,很多都会认为:客户端与服务器最终的打印数据接收或者发送条数都该是一致的,1000条发送打印,1000条接收打印,长度都为1000.但是,事实上并不是这样,发送打 ...

  6. Java网络编程快速上手(SE基础)

    参考资料:百度百科TCP协议 本文涉及Java IO流.异常的知识,可参考我的另外的博客 一文简述Java IO 一文简述JAVA内部类和异常 1.概述 计算机网络相关知识: OSI七层模型 一个报文 ...

  7. SOCKET网络编程细节问题(4)

    SOCKET网络编程快速上手(二)——细节问题(4) 5.慢系统调用及EINTR 还记得前面readn和writen函数么?里面有个EINTR,现在就来谈谈这个,这个很重要. Linux世界有个叫信号 ...

  8. SOCKET网络编程细节问题3

    SOCKET网络编程快速上手(二)——细节问题(3) 3.SIGPIPE问题 人怕牺牲,我们写的程序也一样,人有死不瞑目,程序又何尝不是?程序跑着跑着,突然就崩掉了.好一点的牺牲前告诉你些打印,差点的 ...

  9. SOCKET网络编程细节问题(2)

    SOCKET网络编程快速上手(二)——细节问题(2) 2.TCP数据包接收问题 对初学者来说,很多都会认为:客户端与服务器最终的打印数据接收或者发送条数都该是一致的,1000条发送打印,1000条接收 ...

随机推荐

  1. 关于JavaScript中计算精度丢失的问题

    摘要: 由于计算机是用二进制来存储和处理数字,不能精确表示浮点数,而JavaScript中没有相应的封装类来处理浮点数运算,直接计算会导致运算精度丢失. 为了避免产生精度差异,把需要计算的数字升级(乘 ...

  2. 【设计模式】Abstract Factory模式

    抽象工厂模式是工厂方法模式的进一步强化.当工厂函数仅仅须要产生一种类型的产品(全部产品都继承自同一抽象基类)时,使用工厂方法模式就可以. 可是.当用户程序须要创建多种类型的产品,而这些产品又有一定的内 ...

  3. MSSQL发现第五到数据的第十

    第二十数据查询数据库,第十条数据,两起案件: 1,ID是连接的,当然这样的情况比較好查.直接SELECT就能够了.取ID大于5小于10就能够了, 这样的情况比較少. 2.ID不是连接的.假设要取第五条 ...

  4. AngularJS之使用服务封装

    AngularJS之使用服务封装可复用代码   创建服务组件 在AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数, 然后使用模块的provider方法进行登记: / ...

  5. UITableView刷新局部

    //局部section刷新 NSIndexSet *nd = [[NSIndexSet alloc] initWithIndex:1]; //刷新第二个section [self.tableView ...

  6. Google maps API开发

    原文:Google maps API开发 Google maps API开发(一) 最近做一个小东西用到google map,突击了一下,收获不小,把自己学习的一些小例子记录下来吧 一.加载Googl ...

  7. Hibernat之关系的处理多对多

    第一步:编写两个pojo,比如一个学生表一个课程表  这里使用注解. 需要 课程表: package com.qcf.pox; import java.util.HashSet; import jav ...

  8. java之集合框架使用细节及常用方法

    集合类的由来:   对象用于封装特有数据,对象多了需要存储,如果对象的个数不确定.  就使用集合容器进行存储. 集合特点: 1,用于存储对象的容器. 2,集合的长度是可变的. 3,集合中不可以存储基本 ...

  9. ubuntu eclipse android搭建

    1.eclipse加入android adt: 终端:sudo gedit /etc/hosts 加入: #for android 173.194.72.93 dl.google.com 173.19 ...

  10. C#中使用REDIS

    C#中使用REDIS 上一篇>> 摘要 上一篇讲述了安装redis客户端和服务器端,也大体地介绍了一下redis.本篇着重讲解.NET4.0 和 .NET4.5中如何使用redis和C# ...