2013-5-28 hill
参考《UNIX网络编程第一卷》P143 6.3select函数
这个函数允许进程指示内核等待多个事件中的任何一个发生,并且,仅在一个或者多个事件发生,或者经过某个指定的时间之后,才唤醒进程。
作为一个例子,我们可以调用函数select并通知内核仅仅在下列情况发生时才返回:
1 集合{1, 4, 5} 中的任何描述符准备好读操作。
2 集合{2, 7} 中的任何描述符准备好写操作。
3 集合{1, 4} 中任何描述符有异常条件等待处理。
4 经过了 10.2 秒钟。
也就是说,通知内核我们对哪些描述符感兴趣(读、写或者异常)以及等待多长时间。我们所关心的描述符不仅仅限制于socket套接口,可以是任何描述符,例如串口等等。
源自Berkeley的实现已经允许任何描述符的I/O复用。刚开始,SVR3还限制I/O复用只适用于流设备(第33章)的描述符,但SVR4中就没有了这个限制。
函数的格式如下:
int select(int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
我们从此函数的最后一个参数开始介绍,它告诉内核等待一组指定的描述符字中的任一个准备好可以花多长时间,结构 timeval 指定了秒数和微秒数成员,定义如下:
struct timeval
{
long tv_sec; //秒数
long tv_usec; //微秒数
};
有三种可能:
1 永远等待下去,仅在有一个描述符准备好I/O时才返回,此时,我们将参数timeout设置为NULL空指针,就表示永远等待。
2 等待固定的时间,在有一个描述符准备好I/O时返回,但不超过由timeout参数所指定的timeval结构中指定的时间。
3 根本不等待,检查完描述符字之后,马上立即返回,这就是称为轮询(polling)。为了实现这一点,参数timeout必须指向一个timeval结果,但是 timeval 结构中的时间值必须设置为0值。
在前两种情况的等待中,如果进程捕获了一个信号,并且从信号处理函数返回,那么,等待一般被中断。
源自Berkeley的内核从不自动重启函数select(TCPV2第527页),但,如果在安装信号处理程序时指明标志SA_RESTART,SVR4就自动重启被中断的select。这意味着,为了实现可移植性,我们在捕获信号的时候,必须准备好函数select返回EINTR错误。
虽然结构timeval为我们指定了一个微秒级别的分辨率,但内核支持的分辨率却要粗糙得多。例如,很多Unix内核将超时值向上舍入10ms的倍数。另外还有调度延迟现象,即定时器到后内核还需要花一点时间调度相应进程的运行。
注意:原文P144讲解timeout参数是const格式,那么,该参数在select(); 内是不被修改的。但是,后面的小字部分说到:
现在的Linux系统修改了结构timeval,因此,为了可移植性,需要假设结构timeval在select(); 返回之后,没有定义,要像再次使用,必须初始化。所以,在每次调用select(); 之前,必须对它进行初始化。Posix. 1g 规定使用限定词const。
那么,经过自己的测试,timeout 参数是在select(); 函数返回之后是被修改的,在Fedora14 系统中使用 man select 手册查看 select 函数的定义,发现最后一个参数并没有定义为const类型。所以,该参数在select(); 函数中是可以修改的。例如,timeout设置为等待10秒钟超时,而select(); 等待8秒钟之后,就有信号到底,那么,还剩余2秒钟才到超时,那么,timeout的时间被修改为2秒钟。就是,select(); 返回了,还剩下多少时间才到超时。
中间的三个参数readset, writeset 和 exceptset 指定我们要让内核测试读、写 和 异常条件所需的描述字。现在只支持两个异常条件:
1 套接口带外数据的到达,对此,我们在第21章中再作详细的描述。
2 控制状态信息的存在,可从一个已经置为分组方式的伪终端主端读到。本卷我们不讨论伪终端。
有一个设计上的问题,即如何为这三个参数的每一个指定一个或多个描述字值。函数select 使用描述字集,它一般是一个整数数组,每个数中的每一位对应一个描述字。例如,用32位整数,则数组的第一个元素对应于描述字0~31,数组中的第二个元素对应于描述字32~63,以此类推。所有的实现细节都与应用程序无关,它们隐藏在数据类型fd_set和下面的4个宏中:
void FD_ZERO(fd_set *fdset); //clear all bits in fdset
void FD_SET(int fd, fd_set *fdset); //turn on the bit for fd in fdset
void FD_CLR(int fd, fd_set *fdset); //turn off the bit for fd in fdset
int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset ?
我们分配一个fd_set数据类型的描述符集合,并且用这些宏设置、测试集合中的每一位,我们还可以使用C语言中的赋值语句将其赋值成另外一个描述符集合。
我们所讨论的每个描述符字占用整数数组中一位的方法仅仅是函数select的可能实现之一。不过,将描述符字集合中的每个描述符称呼为位(bit)是很常见的,如“打开读集合中表示监听描述符字的位”。
在6.10节中,我们将看到函数poll(); 用了一个完全不同的表示方法:一个可变长的结构数组,每个结构代表一个描述符。
例如,为了定义一个fd_set类型的变量,并且打开描述符1、4和5的相应位,可以如下:
fd_set rset;
FD_ZERO(&rset); //initialize the set, all bits off
FD_SET(1, &rset); //turn on bit for fd 1
FD_SET(4, &rset); //turn on bit for fd 4
FD_SET(5, &rset); //turn on bit for fd 5
对集合的初始化是很重要。如果集合作为一个自动变量分配而未初始化,那将导致不可预测的后果。
如果我们对某个条件不感兴趣,函数select的三个中间参数readset, writeset和exceptset 中相应参数就可以设置为空指针。实际上,如果三个指针均为空,我们就有了一个比Unix的函数sleep更为精确的定时器。函数poll提供相似功能。APUE的图C.9和C.10给出了一个用select和poll实现的汗水sleep_us,它的睡眠单位为微妙。
参数maxfdp1指定被测试的描述字个数,它的值是要被测试的最大描述字加1(因此,我们将此参数命名为maxfdp1),描述字0、1、2 ……. 一直到 maxfdp1 – 1 均被测试。
头文件
例如,前面我们给出打开描述符1、4和5的代码,其maxfdp1的值应该是6,是6而不是5的原因在于:我们指定的是描述符的个数,而不是最大值。描述符是从0开始,它是内核管理文件结构数组中的一个下标。
我们的推理是:文件描述符只是内核管理文件结构数组中的一个下标,从0开始。而内核在处理select(); 函数的时候,它必须根据 maxfdp1 去查找文件结构数组,伪操作代码如下:
int i;
for(i = 0; i < maxfdp1; i++)
{
//假设 管理文件结构的数组是: file_struct_array[]
//所以,操作 file_struct_array[i],下标 i 是 0 ~ maxfdp1 - 1
file_struct_array[i];
}
例如,上面分析的例子,处理文件描述符1、4和5,最大的文件描述符是5,那么,它存放在下标是5的元素中 file_struct_array[5],那么,在处理如下的for循环:
for(i = 0; i < maxfdp1; i++)
这个时候,为了能够处理到 file_struct_array[5] 元素,maxfdp1 必须设置为 6,这样,循环才能够处理0 ~ 5,能够处理到 file_struct_array[5] 元素。
其实,我们可以推理,内核定义file_struct_array[ ] 数组的时候,知道其长度。那么,在处理select(); 函数的时候,可以省略maxfdp1 这个参数,默认扫描完整个file_struct_array[ ] 数组就可以。但是,这样效率很低。因为每个fd_set都为许多描述符(一般为1024)开有空间,但这要比普通进程所用的数量大得多。内核正是通过在进程与内核间不拷贝不需要的描述符部分和不测试常为0的相应位来获得效率的。
其实,在Windows环境下也提供了select(); 这个 API 接口,但是,它的第一个参数可以忽略设置为0,那么,我们推理:这就是因为Windows内核在处理select(); 这个API接口的时候,默认扫描完整个文件结构数组。
函数select修改由指针readset, writeset和exceptset所指的描述符集合。这三个参数均为“值 – 结果”参数。当我们调用函数时,指定我们所关心的描述符集合,当返回时,结果指示哪些描述符以及准备好。返回时,我们用宏FD_ISSET来测试结构fd_set中的描述字。描述字集合中任何与没有准备好的描述字相对应的位返回时清成0。为此,每次调用select时,我们都得到将所有描述符集合中关心的位设置为1。
此函数的返回值表示跨所有描述符集合的已经准备好的总位数。如果在任何描述符准备好之前,定时器到达,则返回0。返回 -1 表示有错(这是可能发生的,例如 函数被一个捕获的信号所中断)。
注意:当一个套接口出错的是,它由select标记为即可读又可写。
当select(); 正在阻塞等待的时候,它能够被信号中断返回。针对这样的情况,可以使用pselect(); 函数,屏蔽信号。
该函数的第一个参数必须是最大文件描述符加1。根据其定义如下:
int select(int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
就是说,maxfdp1 参数必须是监听的文件描述符最大值加1。
调用一次 select(); 成功之后,fd 在 fd_set 集合中被置位,所以,可以使用FD_ISSET 来检测到 fd 有信号,然后,操作 fd。
例如,使用select(); 函数来监听一个“可读操作”的文件描述符集合fd_set,里面有1、2和5这样3个文件描述符被监听。
假设,现在fd = 5这个文件描述符指向的文件结构中有 10 个字节的数据可以读取,那么,select(); 就返回,然后,通过FD_ISSET(5, &fd_set); 检测到 fd = 5有数据可以读取,然后,读取完这 10 个字节的数据。
道理上,fd = 5 指向的文件结果是没有数据了,但是,此时,再次使用FD_ISSET(5, &fd_set); 来检测 fd = 5的状态,发现它还是有信号,然后,调用 read(); 去读取 fd = 5中的数据,会发现没有读取到任何数据。
所以,我们可以知道:select(); 调用成功之后,FD_ISSET(5, &fd_set); 只是检测到 fd = 5 在 fd_set 中被相应地置位为1,表示有信号要处理。那么,我们处理完数据之后,fd 在 fd_set 中被置位还是为1,没有改变,表示还有信号要处理,其实,现在数据已经被处理完了。所以,我们在再次操作select(); 之前,必须把已经处理过的信号清除掉,然后,再重新调用select(); 函数来进行监听。
所以,如果我们需要多次调用select(); 函数,正确的操作流程是:
select(); 监听 |
有信号处理 |
处理文件描述符 |
重新设置监听描述符集合 |
设置监听描述符集合 |
否 |
是 |
1 设置select 循环坚挺的文件描述符,如果需要等待超时,可以设置超时时间。
2 调用select(); 阻塞等待监听,如果监听到有信号,执行下面步骤3。
3 使用FD_ISSET 检测到 fd 在fd_set 中是否被置位,如果是,表示有信号需要处理,执行下面步骤4。
4 操作 fd 文件描述符。
5 操作完 fd 文件标识符之后,相当是 fd 没有数据需要处理了,表示 fd 在 fd_set 中没有信号,所以,应该使用FD_SET 宏,重新把 fd 添加到 fd_set 集合中,这样,在fd_set集合中就把 fd 相应的位标志给清除,表示没有置位,就是没有信号要处理。这样,就可以再次调用 select(); 函数来监听该套接口。
其实,上图中的流程可以修改为如下:
select(); 监听 |
有信号处理 |
处理文件描述符 |
设置监听描述符集合 |
否 |
是 |
如下是一个读取数据的例子,该函数是指定从 fd 文件描述符中读取 len 个字节的数据到 data[ ] 缓存中。
int operate_read(int fd, INT8U *data, INT32U len)
{
struct timeval wait_time;
fd_set read_set, error_set;
int ret = 0;
INT32U have_read_len = 0, time_out_count = 0;
if((-1 == fd) || (NULL == data) || (len < 0))
{
printf("%s(%d)%s() argument error \n", __FILE__, __LINE__, __FUNCTION__);
return -1;
}
time_out_count = 0;
while(have_read_len < len)
{
bzero(&wait_time, sizeof(struct timeval));
bzero(&read_set, sizeof(fd_set));
bzero(&error_set, sizeof(fd_set));
wait_time.tv_sec = 0;
wait_time.tv_usec = 800000;
FD_ZERO(&read_set);
FD_SET(fd, &read_set);
FD_ZERO(&error_set);
FD_SET(fd, &error_set);
//========================================================
if(12 == time_out_count)
{
printf("wait to read, time out! \n");
return have_read_len;
}
ret = select(fd + 1, &read_set, NULL, &error_set, &wait_time);
if(0 == ret)
{
time_out_count++;
printf("read select time out \n");
continue;
}
else if(-1 == ret)
{
perror("select() error: ");
continue;
}
if(FD_ISSET(fd, &error_set))
{
printf("%s(%d) socket descript (fd) error! \n", __FILE__, __LINE__);
return -1;
}
if(FD_ISSET(fd, &read_set))
{
ret = read(fd, &data[have_read_len], (len - have_read_len));
//====================================================
if(0 == ret)
{
//perror("ret = 0, error: ");
}
//====================================================
if(-1 == ret)
{
perror("read() error: ");
//return 1;
exit(1);
}
have_read_len += ret;
printf("ret = %d, have_read_len = %u, len = %u\n", ret, have_read_len, len);
}
}//end of while
return have_read_len;
}
对于select(); 最后一个参数,如果设置一个时间值,就表示调用select(); 函数来等待信号到达的超时时间。如果设置为 10秒钟,那么,调用select(); 等待10秒钟之后,如果监听的文件描述符集合中都没有信号到达,那么,就结束select(); 函数的阻塞等待,并返回 0值,表示超时返回。
那么,对于这个参数的设置,是传递指针,我们推理:在select(); 函数中会修改这个参数的只,如果,经过测试发现:select(); 返回之后,最后一个参数的值被修改的,该参数被修改的值是select(); 检测到信号之后,还剩下多少时间才超时。
例如,设置超时等待时间是10秒钟,那么,select(); 等待了 8 秒钟之后,才检测到监听的描述符集有信号,那么,select(); 函数返回,此时,最后一个参数是剩下多长时间才到超时,应该等待超时是10秒钟,而等待了8秒就有信号,那么,还剩下2秒钟才到超时。
例如,设置等待超时是10秒钟,那么,select(); 阻塞等待,经过 10 秒钟之后,没有等待到信号,就超时返回。这个时候,最后一个参数的值被设置为0,那么,如果再次调用select(); 来轮询等待,就必须等重新设置最后一个参数,因为,它的值已经被修改为0了。
所以,最后一个参数的是被修改的。那么,当我们多次调用select(); 来监听信号的时候,必须重新设置最后一个参数。
所以,如同上面“1.3 select使用注意点2”分析的那样,当再次使用select(); 的时候,必须重新设置已经处理过的文件描述符 和 超时时间。相应的处理机制可以参考上面“1.3 select 使用注意点2”。
/*
* server_sock.h
*
* Created on: May 28, 2013
* Author: weikaifeng
*/
#ifndef SERVER_SOCK_H_
#define SERVER_SOCK_H_
#include
#include
#include
#define debug_printf(str) \
printf("%s(%d)%s\n", __FILE__, __LINE__, str);
#define perror_printf(str) \
do{ \
char buf[64]; \
snprintf(buf, 64, "%s(%d)%s", __FILE__, __LINE__, str); \
perror(buf); \
}while(0)\
class server_sock
{
int ser_fd; //服务器的套接口
int listen_port; //服务器监听的套接口
int listen_sock_max; //listen(); 函数能够监听的最大连接数
public:
server_sock(int port);
virtual ~server_sock();
void work(); //服务器的主工作函数
int init_server(); //初始化服务器
int create_sock_listen();
int accept_client();
int set_port_reuse();
int operate_client(int fd); //操作客户端
};
#endif /* SERVER_SOCK_H_ */
/*
* server_sock.cpp
*
* Created on: May 28, 2013
* Author: weikaifeng
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "server_sock.h"
//=================================================================
//构造函数
//=================================================================
server_sock::server_sock(int port)
{
ser_fd = -1;
listen_port = port;
listen_sock_max = 10;
}
//=================================================================
//析构函数
//=================================================================
server_sock::~server_sock()
{
}
//=================================================================
//服务器的主工作函数
//=================================================================
void server_sock::work()
{
int client_fd = -1;
while(1)
{
client_fd = accept_client();
if(-1 == client_fd)
{
break;
}
operate_client(client_fd);
}
}
//=================================================================
//初始化服务器
//成功返回 0,失败返回 1
//=================================================================
int server_sock::init_server()
{
int ret = 1;
ret = create_sock_listen();
if(1 == ret)
{
return 1;
}
return 0;
}
//=================================================================
//创建 server 监听指定的 端口号
//成功返回 0,失败返回 1
//=================================================================
int server_sock::create_sock_listen()
{
int ret = 1;
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(listen_port);
ser_fd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == ser_fd)
{
perror_printf("create socket error:");
return 1;
}
//设置服务器的端口可以重用
ret = set_port_reuse();
if(1 == ret)
{
return 1;
}
ret = bind(ser_fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in));
if(-1 == ret)
{
perror_printf("bind() error: ");
return 1;
}
ret = listen(ser_fd, listen_sock_max);
if(-1 == ret)
{
perror_printf("listen() error: ");
return 1;
}
debug_printf("Server create the socket, and listening .....\n");
return 0;
}
//=================================================================
//接收一个客户端的链接,当前的 ser_fd 是 阻塞模式,所以,调用 accept(); 函数
//会一直阻塞,等待有客户端链接到达。
//成功返回 客户端链接的 socket 文件描述符,失败返回 -1
//=================================================================
int server_sock::accept_client()
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = -1;
int client_fd = -1;
char client_ip[INET_ADDRSTRLEN];
int client_port = 0;
client_addr_len = sizeof(struct sockaddr);
client_fd = accept(ser_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if(-1 == client_fd)
{
perror_printf("accept error:");
return -1;
}
bzero(client_ip, INET_ADDRSTRLEN);
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
client_port = ntohs(client_addr.sin_port);
printf("client connect...ip = %s, port = %d\n", client_ip, client_port);
return client_fd;
}
//=================================================================
//设置服务器监听的 端口 可以被多个 socket 套接口使用。
//成功返回 0,失败返回 1
//=================================================================
int server_sock::set_port_reuse()
{
int on_or_off = 1; //为 非0 表示打开标记
int len = sizeof(int);
int ret = 0;
ret = setsockopt(ser_fd, SOL_SOCKET, SO_REUSEADDR, &on_or_off, len);
if(-1 == ret)
{
perror_printf("setsockopt error!");
return 1;
}
return 0;
}
#define BUF_LEN 32
//=================================================================
//操作客户端
//成功返回 0,失败返回 1
//=================================================================
int server_sock::operate_client(int fd)
{
char send_buf[BUF_LEN];
int ret = 0;
for(int i = 0; i < BUF_LEN; i++)
{
send_buf[i] = i;
}
sleep(3);
ret = write(fd, send_buf, BUF_LEN);
if(BUF_LEN != ret)
{
perror_printf("write error!");
return 1;
}
return 0;
}
#include
#include
#include
#include
#include "server_sock.h"
int main(int argc, char* argv[])
{
int port = 8861;
int ret = 1;
server_sock server(port);
ret = server.init_server();
if(1 == ret)
{
return 1;
}
server.work();
printf("hehe...\n");
return 0;
}
#ARM =arm-linux-
TARGET = server
ARM =
CC = $(ARM)g++ -c
DCC = $(ARM)g++ -o $@
HEADERS = server_sock.h
OBJ = server_sock.o main.o
%.o:%.cpp
$(CC) $< -o $@
$(TARGET): $(OBJ)
$(DCC)$(OBJ)
.PHONY:clean
clean:
rm -rf *.o
/*
* client_sock.h
*
* Created on: May 28, 2013
* Author: weikaifeng
*/
#ifndef CLIENT_SOCK_H_
#define CLIENT_SOCK_H_
#include
#include
#include
#define debug_printf(str) \
printf("%s(%d)%s\n", __FILE__, __LINE__, str);
#define perror_printf(str) \
do{ \
char buf[64]; \
snprintf(buf, 64, "%s(%d)%s", __FILE__, __LINE__, str); \
perror(buf); \
}while(0)\
class client_sock
{
int fd; //链接到 server 的 socket 文件描述符
int port; //链接到 server 的端口号
char *ip; //server 的 IP 地址
struct timeval read_wait_time; //等待 读取 数据的时间
struct timeval write_wait_time; //等待 写入 数据的时间
fd_set read_set;
fd_set read_err_set;
fd_set write_set;
fd_set write_err_set;
public:
client_sock(char *ser_ip, int ser_port);
virtual ~client_sock();
void work(); //客户端的主工作函数
int init_client(); //初始化客户端
int connect_to_server(); //创建 socket 链接到服务器
void set_read_select_attr(); //设置 read 操作时 select(); 参数的属性
void show_data(char* data, int len);
int set_unblock_mode(); //设置为非阻塞模式
};
#endif /* CLIENT_SOCK_H_ */
/*
* client_sock.cpp
*
* Created on: May 28, 2013
* Author: weikaifeng
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "client_sock.h"
//=================================================================
//构造函数
//=================================================================
client_sock::client_sock(char *ser_ip, int ser_port)
{
fd = -1;
port = ser_port;
ip = (char*)malloc(strlen(ser_ip) + 1);
//把 ser_ip 指向的字符串,连同字符串结束符 '\0' 也复制到 ip 内存空间中
memcpy(ip, ser_ip, strlen(ser_ip) + 1);
}
//=================================================================
//析构函数
//=================================================================
client_sock::~client_sock()
{
}
//=================================================================
//创建 socket 链接到服务器
//成功返回 0,失败返回 1
//=================================================================
int client_sock::connect_to_server()
{
struct sockaddr_in server_addr;
int ret = 0;
bzero(&server_addr, sizeof(struct sockaddr_in));
//设置链接 server 使用的 协议
server_addr.sin_family = AF_INET;
//设置链接 server 的 IP 地址
ret = inet_pton(AF_INET, ip, &server_addr.sin_addr);
if(1 != ret)
{
perror_printf("inet_pton error:");
return 1;
}
//设置链接 server 的端口号
server_addr.sin_port = htons(port);
//创建 socket 套接口
fd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == fd)
{
perror_printf("create socket error:");
return 1;
}
//把创建好的 socket 套接口 fd 链接到 server
ret = connect(fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in));
if(-1 == ret)
{
perror("connect to server error:");
return 1;
}
printf("connect to server success! ip = %s, port = %d \n", ip, port);
return 0;
}
#define BUF_LEN 32
//处理 select(); 函数的返回信息
/* //下面这个宏定义有错误,因为,里面是有的 continue, break 关键字没有能够应用到调用 deal_select_ret 宏的地方,而只限制在定义该红的do…while(0) 内部。
#define deal_select_ret(ret)\
do{ \
if(0 == ret){ \
perror_printf("select time out:");\
continue; \
} \
if(-1 == ret){\
perror_printf("select error:");\
continue; \
}\
}while(0) \
//处理 select(); 监听到套接口有错误
#define deal_err_fd(var1, var2) \
do{ \
if(FD_ISSET(var1, var2)) \
{ \
debug_printf("socket descript fd error:"); \
break; \
} \
}while(0)\
*/
#define deal_select_ret(ret)\
{ \
if(0 == ret){ \
perror_printf("select time out:");\
continue; \
} \
if(-1 == ret){\
perror_printf("select error:");\
continue; \
}\
} \
//处理 select(); 监听到套接口有错误
#define deal_err_fd(var1, var2) \
{ \
if(FD_ISSET(var1, var2)) \
{ \
debug_printf("socket descript fd error:"); \
break; \
} \
}\
//=================================================================
//客户端的主工作模式
//=================================================================
void client_sock::work()
{
int ret = 0;
char recv_buf[BUF_LEN];
bzero(recv_buf, BUF_LEN);
while(1)
{
set_read_select_attr();
ret = select(fd + 1, &read_set, NULL, &read_err_set, &read_wait_time);
printf("sec = %d \n", read_wait_time.tv_sec);
printf("usec = %d \n", read_wait_time.tv_usec);
deal_select_ret(ret);
deal_err_fd(fd, &read_err_set);
if(FD_ISSET(fd, &read_set))
{
debug_printf("fd have signal!");
ret = read(fd, recv_buf, BUF_LEN);
if(-1 == ret)
{
perror("read err!");
continue;
}
show_data(recv_buf, BUF_LEN);
}
if(FD_ISSET(fd, &read_set))
{
debug_printf("fd have signal!");
ret = read(fd, recv_buf, BUF_LEN);
if(-1 == ret)
{
perror("read err!");
continue;
}
show_data(recv_buf, BUF_LEN);
}
}//end of while(1)
}
//=================================================================
//初始化客户端
//成功返回 0,失败返回 1
//=================================================================
int client_sock::init_client()
{
int ret = 1;
ret = connect_to_server();
if(1 == ret)
{
return 1;
}
ret = set_unblock_mode();
if(1 == ret)
{
return 1;
}
return 0;
}
//=================================================================
//设置 select 的各种参数
//=================================================================
void client_sock::set_read_select_attr()
{
bzero(&read_wait_time, sizeof(struct timeval));
bzero(&read_set, sizeof(fd_set));
bzero(&read_err_set, sizeof(fd_set));
read_wait_time.tv_sec = 5;
read_wait_time.tv_usec = 0;
FD_ZERO(&read_set);
FD_SET(fd, &read_set);
FD_ZERO(&read_err_set);
FD_SET(fd, &read_err_set);
}
//=================================================================
//显示信息
//=================================================================
void client_sock::show_data(char* data, int len)
{
for(int i = 0; i < len; i++)
{
printf("%d ", data[i]);
}
printf("\n");
}
//=================================================================
//设置为非阻塞模式
//成功返回 0,失败返回 1
//=================================================================
int client_sock::set_unblock_mode()
{
int val = 0;
val = fcntl(fd, F_GETFL, 0);
if(-1 == val)
{
perror_printf("fcntl get error!");
return 1;
}
val |= O_NONBLOCK;
val = fcntl(fd, F_SETFL, val);
if(-1 == val)
{
perror_printf("fcntl set error!");
return 1;
}
return 0;
}
#include
#include
#include
#include
#include "client_sock.h"
int main(int argc, char* argv[])
{
int ret = 1;
client_sock client((char*)"127.0.0.1", 8861);
ret = client.init_client();
if(1 == ret)
{
return 1;
}
client.work();
printf("hehe...\n");
return 0;
}
#ARM =arm-linux-
TARGET = clent
ARM =
CC = $(ARM)g++ -c
DCC = $(ARM)g++ -o $@
HEADERS = client_sock.h
OBJ = client_sock.o main.o
%.o:%.cpp
$(CC) $< -o $@
$(TARGET): $(OBJ)
$(DCC)$(OBJ)
.PHONY:clean
clean:
rm -rf *.o
在测试select(); 的最后一个参数是否被修改的时候,可以修改server 的代码,当接收到一个客户端的连接之后,休眠 3秒钟再返回数据包,那么,client接收到数据包就延迟,再输出该参数,可以看到它的变化。
当select(); 在阻塞监听描述符集的时候,如果当前进程接收到信号,例如SIGALRM这样的时钟信号,select(); 函数也会马上返回。
在上面“1.5的测试例子”,我们增加如下一个处理逻辑:
1 使用setitimer(); 函数来定义一个定时器,让内核定时给该进程发送SIGALRM信号。
2 select(); 函数的等待超时设置长一些,以便SIGALRM信号能够触发,观察select(); 函数是否返回。
那么,在上面“1.5测试例子”中增加如下代码:
1 增加一个set_alarm_time(); 函数,注册定时器:
int client_sock::set_alarm_time()
{
signal(SIGALRM, sig_handler);
struct itimerval alarm_time; //触发SIGALRM 信号的时间机制
//开始注册信号之后,经过1 秒钟,就触发SIGALRM 信号
alarm_time.it_value.tv_sec = 1;
alarm_time.it_value.tv_usec = 0;
//触发SIGALRM 信号之后,每隔2 秒钟再触发一次
alarm_time.it_interval.tv_sec = 2;
alarm_time.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &alarm_time, NULL);
return 0;
}
2 定义SIGALRM 的信号处理函数sig_handler
//信号处理函数,参数sig_num 是触发给函数的信号ID
void sig_handler(int sig_id)
{
//根据信号ID来判断,当前是触发了那一个信号
switch(sig_id)
{
case SIGALRM:
{
printf("deal sig_id == SIGALRM = %d \n", SIGALRM);
break;
}
default:
{
printf("unknow sigid = %d \n", sig_id);
break;
}
}
}
3 设置定时器
在init_client(); 函数中调用 set_alarm_time(); 函数。
最终,运行程序的结果如下:
[weikaifeng@weikaifeng client]$ ./clent
connect to server success! ip = 127.0.0.1, port = 8861
client_sock.cpp(121)fd have signal!
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
deal sig_id == SIGALRM = 14
client_sock.cpp(116)select error:: Interrupted system call
deal sig_id == SIGALRM = 14
client_sock.cpp(116)select error:: Interrupted system call
可以看到,select(); 函数返回 -1,表示出错!其实,它是被SIGALRM信号中断返回。如果在等待的时候被新行中断,可以使用pselect(); 函数。
函数pselect(); 是由Posix.lg 发明的,其格式如下:
int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
该函数对应 select(); 函数有 3 个变化:
1 pselect(); 是的超时等待时间结构是 timespec,其定义如下:
struct timespec
{
time_t tv_sec; //秒数
long tv_nsec; //纳秒
};
可以精确到纳秒级别,而select函数只能够精确到毫秒级别。
2 pselect(); 的超时等待时间结构是const类型,表示,该参数在pselect(); 中不能够修改。所以,当我们需要循环多次调用 pselect(); 函数的时候,可以不用每次在调用pselect(); 之前都对超时等待时间进行初始化。而select(); 的超时等待时间是有变化的,具体参考《1.4 select使用注意点 3》。
3 函数pselect(); 增加了第六个参数,它指向一个信号集。该信号集就装载了在执行 pselect(); 函数的时候,那些信号可以屏蔽掉。
如下是一个测试的例子,思路是:
1 当前进程自己定时触发SIGALRM信号,然后,在pselect(); 函数中屏蔽该信号,具体参考上面“2.1 select被信号中断”。
2 增加处理SIGUSR1信号,如果沿用上面“2.1 select被信号中断”处理机制的话,是单进程模型,整个流程都会阻塞在pselect(); 函数调用处,不容易观察SIGUSR1信号的发送和接收,所以,使用多进程来处理:在main(); 处使用fork(); 函数创建一个子进程,在子进程中运行client 网络通信,在父进程中定时向子进程发送SIGUSR1信号。
参考“1.5的测试例子”,有如下的修改逻辑:
1 修改client_sock 的 work(); 函数,用 pselect(); 函数来处理。
void client_sock::work()
{
int ret = 0;
char recv_buf[BUF_LEN];
sigset_t sigset;
struct timespec t;
bzero(recv_buf, BUF_LEN);
sigemptyset(&sigset);
sigaddset(&sigset, SIGALRM); //把 SIGALRM 信号添加到 sigset 信号集中
//sigaddset(&sigset, SIGUSR1);
bzero(&t, sizeof(struct timespec));
t.tv_sec = 10;
t.tv_nsec = 0;
while(1)
{
bzero(&read_set, sizeof(fd_set));
bzero(&read_err_set, sizeof(fd_set));
FD_ZERO(&read_set);
FD_SET(fd, &read_set);
FD_ZERO(&read_err_set);
FD_SET(fd, &read_err_set);
ret = pselect(fd + 1, &read_set, NULL, &read_err_set, &t, &sigset);
deal_select_ret(ret);
deal_err_fd(fd, &read_err_set);
if(FD_ISSET(fd, &read_set))
{
debug_printf("fd have signal!");
ret = read(fd, recv_buf, BUF_LEN);
if(-1 == ret)
{
perror("read err!");
continue;
}
show_data(recv_buf, BUF_LEN);
}
}//end of while(1)
}
2 定义set_alarm_time(); 函数注册信号
int client_sock::set_alarm_time()
{
signal(SIGUSR1, sig_handler); //注册 SIGUSR1 信号
signal(SIGALRM, sig_handler); //注册 SIGALRM 信号
struct itimerval alarm_time; //触发SIGALRM 信号的时间机制
//开始注册信号之后,经过1 秒钟,就触发SIGALRM 信号
alarm_time.it_value.tv_sec = 1;
alarm_time.it_value.tv_usec = 0;
//触发SIGALRM 信号之后,每隔2 秒钟再触发一次
alarm_time.it_interval.tv_sec = 2;
alarm_time.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &alarm_time, NULL);
return 0;
}
3 编写信号处理函数 sig_handler();
void sig_handler(int sig_id)
{
//根据信号ID来判断,当前是触发了那一个信号
switch(sig_id)
{
case SIGUSR1:
{
printf("deal sig_id == SIGUR1 = %d \n", SIGUSR1);
break;
}
case SIGALRM:
{
printf("deal sig_id == SIGALRM = %d \n", SIGALRM);
break;
}
default:
{
printf("unknow sigid = %d \n", sig_id);
break;
}
}
}
4 设置定时器
在init_client(); 函数中调用 set_alarm_time(); 函数。
5 修改main(); 函数,创建子进程
int main(int argc, char* argv[])
{
pid_t pid;
int ret = 1;
client_sock client((char*)"127.0.0.1", 8861);
ret = client.init_client();
if(1 == ret)
{
return 1;
}
pid = fork();
if(-1 == pid)
{
perror_printf("fork error:");
return 1;
}
if(0 == pid) //子进程
{
//client.work();
client.work_pselect();
}
else
{
//父进程
while(1)
{
sleep(1);
debug_printf("in father process, send SIGUSR1!");
kill(pid, SIGUSR1); //向子进程发送 SIGUSR1 信号)
}
}
printf("hehe...\n");
return 0;
}
6 测试
我们只需要修改 work(); 函数是否把SIGUSR1 和 SIGALRM 信号添加到屏蔽信号集中就可以测试,例如:
1 屏蔽这两个信号,那么,运行的结果如下:
[weikaifeng@weikaifeng client]$ ./clent
connect to server success! ip = 127.0.0.1, port = 8861
client_sock.cpp(172)fd have signal!
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
deal sig_id == SIGALRM = 14
main.cpp(46)in father process, send SIGUSR1!
main.cpp(46)in father process, send SIGUSR1!
deal sig_id == SIGALRM = 14
main.cpp(46)in father process, send SIGUSR1!
main.cpp(46)in father process, send SIGUSR1!
deal sig_id == SIGALRM = 14
main.cpp(46)in father process, send SIGUSR1!
可以看到,内核触发了SIGALRM信号,父进程给子进程发送了SIGUSR1信号,都被屏蔽掉了,pselect(); 函数没有被中断返回。
2 屏蔽SIGALRM信号,不屏蔽SIGUSR1信号,测试的运行结果如下:
[weikaifeng@weikaifeng client]$ ./clent
connect to server success! ip = 127.0.0.1, port = 8861
client_sock.cpp(172)fd have signal!
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
deal sig_id == SIGALRM = 14
main.cpp(46)in father process, send SIGUSR1!
deal sig_id == SIGUR1 = 10
client_sock.cpp(167)select error:: Interrupted system call
main.cpp(46)in father process, send SIGUSR1!
deal sig_id == SIGUR1 = 10
client_sock.cpp(167)select error:: Interrupted system call
可以看到,当父进程发送SIGUSR1信号之后,子进程在pselect(); 函数中的阻塞就被中断返回。
参考《UNIX网络编程第一卷》P161 介绍 pselect 函数的时候,提到了一种方法:使用sigprocmask(); 函数来设置当前进程要屏蔽的信号,这样,pselect(); 函数的最后一个参数就完全没有作用了。这个原理可以完全应用到select(); 函数中,这样,select(); 函数就能够避免被信号中断返回。
原理参考上面“2.4 select 屏蔽信号的方法”。