9. I/O复用
I/O复用使得程序可以同时监听多个文件描述符。通常,网络程序在下列情况下需要使用I/O复用技术。
- 客户端程序要同时处理多个socket。比如本章要讨论的非阻塞connect技术。
9.1 select系统调用
select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
9.1.1 select API
arduino
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
-
nfds参数指定被监听的文件描述符的总数
-
readfds、writefds、exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。传入到内核中会被修改
-
这三个参数都是fd_set结构指针类型,它仅仅包含一个整型数组,每个元素的每1bit标记一个文件描述符
-
通常使用下列宏来访问fd_set结构体中的位:
scss#include <sys/select.h> FD_ZERO(fd_set* fdset); /*清空fd_set集合*/ FD_SET(int fd, fd_set* fdset) /*将给定的文件描述符加入集合之中*/ FD_CLR(int fd, fd_set* fdset) /*将给定的文件描述符从集合中删除*/ int FD_ISSET(int fd, fd_set* fdset) /*检测fd在fdset集合中的状态是否发生变化*/
-
-
timeout参数用来设置select函数的超时时间。如果传递给NULL,则select将一直阻塞,直到某个文件描述符就绪
9.1.2 文件描述符就绪条件
9.1.3 处理带外数据
socket上接收到普通数据和带外数据都将使select返回,但socket处于不同的就绪状态。
scss
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
printf( "ip is %s and port is %d\n", ip, port );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
close( listenfd );
}
char remote_addr[INET_ADDRSTRLEN];
printf( "connected with ip: %s and port: %d\n", inet_ntop( AF_INET, &client_address.sin_addr, remote_addr, INET_ADDRSTRLEN ), ntohs( client_address.sin_port ) );
char buf[1024];
fd_set read_fds;
fd_set exception_fds;
/*给两个fd_set置空*/
FD_ZERO( &read_fds );
FD_ZERO( &exception_fds );
int nReuseAddr = 1;
/*设置socket选项*/
setsockopt( connfd, SOL_SOCKET, SO_OOBINLINE, &nReuseAddr, sizeof( nReuseAddr ) );
while( 1 )
{
memset( buf, '\0', sizeof( buf ) );
/*将fd加入到两个不同的fd_set集合中*/
FD_SET( connfd, &read_fds );
FD_SET( connfd, &exception_fds );
ret = select( connfd + 1, &read_fds, NULL, &exception_fds, NULL );
printf( "select one\n" );
if ( ret < 0 )
{
printf( "selection failure\n" );
break;
}
/*检测对应的文件描述符是否读就绪*/
if ( FD_ISSET( connfd, &read_fds ) )
{
ret = recv( connfd, buf, sizeof( buf )-1, 0 );
if( ret <= 0 )
{
break;
}
printf( "get %d bytes of normal data: %s\n", ret, buf );
}
/*检测对应的文件描述符是否发生异常事件*/
else if( FD_ISSET( connfd, &exception_fds ) )
{
ret = recv( connfd, buf, sizeof( buf )-1, MSG_OOB );
if( ret <= 0 )
{
break;
}
printf( "get %d bytes of oob data: %s\n", ret, buf );
}
}
close( connfd );
close( listenfd );
return 0;
}
服务端两次实验截图:
9.2 poll系统调用
poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。
arduino
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
-
fds参数一个pollfd结构体类型数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。它定义如下:
arduinostruct pollfd { int fd; /*文件描述符*/ short events; /*注册的事件*/ short revents; /*实际发生的事件,由内核填写*/ }
pollfd支持的事件类型如表所示:
事件 描述 是否可作为输入 是否可作为输出 POLLIN 数据(包括普通数据和优先数据)可读 是 是 POLLRDNORM 普通数据可读 是 是 POLLPRI 高优先级数据可读,比如TCP外带数据 是 是 POLLOUT 数据(包括普通数据和优先数据)可写 是 是 POLLWRNORM 普通数据可写 是 是 POLLWRBAND 优先级数据可写 是 是 POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作 是 是 POLLERR 错误 否 是 POLLHUP 挂起 否 是 POLLNVAL 文件描述符没有打开 否 是 -
ndfs参数指定被监听事件集合fds的大小;
-
timeout参数指定poll的超时值,当timeout置为-1时,poll调用将阻塞直到某个事件发生。
9.3 epoll系列系统调用
epoll使用一组函数来完成任务,而不是单个函数。
9.3.1 内核事件表
epoll把用户关心的文件描述符放到内核的一个内核事件表,避免像select和poll那样每次使用都需要重复传入文件描述符集和事件集。但它需要使用一个额外的文件描述符,来标识内核中的这个事件表。这个文件描述符使用下列函数传递:
arduino
#include <sys/epoll.h>
int epoll_create(int size)
- size参数只是对内核的一个提示
- 该函数的返回值将用作epoll系列其他函数的第一个参数,以指定内核事件表
下面的函数用来操作内核事件表:
arduino
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
-
fd参数是要操作的文件描述符
-
op参数则指定操作类型
- EPOLL_CTL_ADD, 往事件表上注册fd上的事件
- EPOLL_CTL_MOD,修改fd上的注册事件
- EPOLL_CTL_DEL,删除fd上的注册事件
-
event参数指定事件类型,它是epoll_event结构指针类型。epoll_event的定义如下:
arduinostruct epoll_event { _uint32_t events; /*epoll事件类型*/ epoll_data_t data; /*用户数据*/ }
其中的events成员描述事件类型。epoll支持的事件类和poll基本相同,在poll对应的宏前加上'E'即可。其中epoll_data_t的定义如下:
arduinotypedef union epoll_data { void* ptr; int fd; /*指定事件所从属的目标文件描述符*/ uint32_t u32; uint64_t u64; } epoll_data_t;
9.3.2 epoll_wait函数
epoll系列系统调用的主要接口是epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件。
arduino
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。
9.3.3 LT和ET模式
[EPOLL模型详解] blog.csdn.net/luseysd/art...
[关于EPOLL的阻塞和非阻塞以及LT和ET模式] blog.csdn.net/luseysd/art...
Level Triggered (LT) 水平触发
socket接收缓冲区不为空 有数据可读 读事件一直触发
socket发送缓冲区不满 可以继续写入数据 写事件一直触发
符合思维习惯,epoll_wait返回的事件就是socket的状态
Edge Triggered (ET) 边沿触发
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件,仅在状态变化时触发事件
ini
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
/*将文件描述符设置为非阻塞的*/
int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
/*
@epollfd:内核时间表的文件描述符
@fd:被操作的目标文件描述符
@enable:指定是否对fd启用ET模式
func:把事件-目标文件描述符添加到内核事件表中
*/
void addfd( int epollfd, int fd, bool enable_et )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if( enable_et )
{
event.events |= EPOLLET;
}
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
/*设置fd文件描述符为非阻塞的*/
setnonblocking( fd );
}
/*
@events:就绪的事件-目标文件描述符数组
@number:events数组个数
@epollfd:内核事件表的文件描述符
@listenfd:监听socket文件描述符
*/
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{
char buf[ BUFFER_SIZE ];
for ( int i = 0; i < number; i++ )
{
/*事件所从属的socket文件描述符*/
int sockfd = events[i].data.fd;
/*用监听socket去完成TCP连接,同时为连接socket注册读事件并设置为LT模式*/
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
/*注意:不管是LT模式还是ET模式,connfd这个连接socket文件描述符总是非阻塞的*/
addfd( epollfd, connfd, false );
}
/*LT模式下连接socket数据可读事件发生,则读取之*/
else if ( events[i].events & EPOLLIN )
{
printf( "event trigger once\n" );
memset( buf, '\0', BUFFER_SIZE );
int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
if( ret <= 0 )
{
close( sockfd );
continue;
}
printf( "get %d bytes of content: %s\n", ret, buf );
}
else
{
printf( "something else happened \n" );
}
}
}
/*
@events:就绪的事件-目标文件描述符数组
@number:events数组个数
@epollfd:内核事件表的文件描述符
@listenfd:监听socket文件描述符
*/
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
char buf[ BUFFER_SIZE ];
for ( int i = 0; i < number; i++ )
{
/*事件所从属的socket文件描述符*/
int sockfd = events[i].data.fd;
/*用监听socket去完成TCP连接,同时为连接socket注册读事件并设置为ET模式*/
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd, true );
}
/*ET模式下连接socket上有读事件发生*/
else if ( events[i].events & EPOLLIN )
{
printf( "event trigger once\n" );
while( 1 )
{
memset( buf, '\0', BUFFER_SIZE );
int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
if( ret < 0 )
{
/*非阻塞的recv,当无数据可读时产生的错误;
ET模式下最好使用非阻塞式的IO,如果使用阻塞式I/O读取函数
这就会导致在数据读完之后,最后一次read阻塞,因为所有数据已经读取完了*/
if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
{
printf( "read later\n" );
break;
}
close( sockfd );
break;
}
/*数据读取完毕*/
else if( ret == 0 )
{
close( sockfd );
}
else
{
printf( "get %d bytes of content: %s\n", ret, buf );
}
}
}
else
{
printf( "something else happened \n" );
}
}
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
/*监听socket*/
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
/*用户空间的事件数组,用来接收就绪的事件*/
epoll_event events[ MAX_EVENT_NUMBER ];
/*内核事件表的文件描述符*/
int epollfd = epoll_create( 5 );
assert( epollfd != -1 );
/*监听socket文件描述符加入到内核事件表中,并设置为ET模式*/
addfd( epollfd, listenfd, true );
while( 1 )
{
int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
if ( ret < 0 )
{
printf( "epoll failure\n" );
break;
}
lt( events, ret, epollfd, listenfd );
//et( events, ret, epollfd, listenfd );
}
close( listenfd );
return 0;
}
客户端发送数据:
LT模式下服务端接收数据:
ET模式下服务端接收数据:
9.3.4 EPOLLONESHOT事件
即使使用ET模式,一个socket上的某个事件还是可能被多次触发。在多线程环境下,容易出现同一个socket上的两次上下文数据被不同的线程读取。我们期望一个socket连接在任何一个时刻都只被一个线程处理,就可以使用EPOLLONESHOT事件实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个事件,且只触发一次。所以每次处理完必须使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
ini
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds
{
int epollfd;
int sockfd;
};
int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
void addfd( int epollfd, int fd, bool oneshot )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
if( oneshot )
{
event.events |= EPOLLONESHOT;
}
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
setnonblocking( fd );
}
void reset_oneshot( int epollfd, int fd )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}
void* worker( void* arg )
{
int sockfd = ( (fds*)arg )->sockfd;
int epollfd = ( (fds*)arg )->epollfd;
printf( "start new thread to receive data on fd: %d\n", sockfd );
char buf[ BUFFER_SIZE ];
memset( buf, '\0', BUFFER_SIZE );
while( 1 )
{
int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
if( ret == 0 )
{
close( sockfd );
printf( "foreiner closed the connection\n" );
break;
}
else if( ret < 0 )
{
if( errno == EAGAIN )
{
reset_oneshot( epollfd, sockfd );
printf( "read later\n" );
break;
}
}
else
{
printf( "get content: %s\n", buf );
sleep( 5 );
}
}
printf( "end thread receiving data on fd: %d\n", sockfd );
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
epoll_event events[ MAX_EVENT_NUMBER ];
int epollfd = epoll_create( 5 );
assert( epollfd != -1 );
addfd( epollfd, listenfd, false );
while( 1 )
{
int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
if ( ret < 0 )
{
printf( "epoll failure\n" );
break;
}
for ( int i = 0; i < ret; i++ )
{
int sockfd = events[i].data.fd;
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd, true );
}
else if ( events[i].events & EPOLLIN )
{
pthread_t thread;
fds fds_for_new_worker;
fds_for_new_worker.epollfd = epollfd;
fds_for_new_worker.sockfd = sockfd;
pthread_create( &thread, NULL, worker, ( void* )&fds_for_new_worker );
}
else
{
printf( "something else happened \n" );
}
}
}
close( listenfd );
return 0;
}
9.4 三组I/O复用函数的比较
9.5 I/O复用的高级应用一:非阻塞connect
9.6 I/O复用的高级应用二:聊天室程序
客户端:把标准输入零拷贝到发送服务器中
ini
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>
#define BUFFER_SIZE 64
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons( port );
int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
close( sockfd );
return 1;
}
pollfd fds[2];
/*标准输入*/
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[0].revents = 0;
fds[1].fd = sockfd;
fds[1].events = POLLIN | POLLRDHUP;
fds[1].revents = 0;
char read_buf[BUFFER_SIZE];
int pipefd[2];
int ret = pipe( pipefd );
assert( ret != -1 );
while( 1 )
{
ret = poll( fds, 2, -1 );
if( ret < 0 )
{
printf( "poll failure\n" );
break;
}
/*TCP连接被对方关闭*/
if( fds[1].revents & POLLRDHUP )
{
printf( "server close the connection\n" );
break;
}
/*在终端打印服务器发送过来的数据*/
else if( fds[1].revents & POLLIN )
{
memset( read_buf, '\0', BUFFER_SIZE );
recv( fds[1].fd, read_buf, BUFFER_SIZE-1, 0 );
printf( "%s\n", read_buf );
}
/*将标准输入的数据经过零拷贝发送给服务端socket*/
if( fds[0].revents & POLLIN )
{
/*为什么要通过管道文件中转?*/
ret = splice( 0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
ret = splice( pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
}
}
close( sockfd );
return 0;
}
服务端:接收客户数据,并把数据发送给每一个登录到该服务器上的客户端(发送者除外)
ini
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <poll.h>
#define USER_LIMIT 5
#define BUFFER_SIZE 64
#define FD_LIMIT 65535
struct client_data
{
sockaddr_in address;
char* write_buf;
char buf[ BUFFER_SIZE ];
};
/*将文件描述符设置为非阻塞*/
int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
/*监听socket*/
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
client_data* users = new client_data[FD_LIMIT];
pollfd fds[USER_LIMIT+1];
/*记录监听事件集合的大小*/
int user_counter = 0;
/*初始化*/
for( int i = 1; i <= USER_LIMIT; ++i )
{
fds[i].fd = -1;
fds[i].events = 0;
}
/*把监听socket放到事件表中*/
fds[0].fd = listenfd;
/*监听读就绪事件和错误事件*/
fds[0].events = POLLIN | POLLERR;
fds[0].revents = 0;
while( 1 )
{
ret = poll( fds, user_counter+1, -1 );
if ( ret < 0 )
{
printf( "poll failure\n" );
break;
}
for( int i = 0; i < user_counter+1; ++i )
{
/*监听socket读就绪*/
if( ( fds[i].fd == listenfd ) && ( fds[i].revents & POLLIN ) )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
continue;
}
/*超出用户数限制*/
if( user_counter >= USER_LIMIT )
{
const char* info = "too many users\n";
printf( "%s", info );
send( connfd, info, strlen( info ), 0 );
close( connfd );
continue;
}
user_counter++;
users[connfd].address = client_address;
setnonblocking( connfd );
fds[user_counter].fd = connfd;
fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR;
fds[user_counter].revents = 0;
printf( "comes a new user, now have %d users\n", user_counter );
}
else if( fds[i].revents & POLLERR )
{
printf( "get an error from %d\n", fds[i].fd );
char errors[ 100 ];
memset( errors, '\0', 100 );
socklen_t length = sizeof( errors );
/*获取并清除socket错误状态*/
if( getsockopt( fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length ) < 0 )
{
printf( "get socket option failed\n" );
}
continue;
}
/*TCP连接被对方关闭*/
else if( fds[i].revents & POLLRDHUP )
{
users[fds[i].fd] = users[fds[user_counter].fd];
close( fds[i].fd );
fds[i] = fds[user_counter];
i--;
user_counter--;
printf( "a client left\n" );
}
/*连接socket读就绪*/
else if( fds[i].revents & POLLIN )
{
int connfd = fds[i].fd;
memset( users[connfd].buf, '\0', BUFFER_SIZE );
ret = recv( connfd, users[connfd].buf, BUFFER_SIZE-1, 0 );
printf( "get %d bytes of client data %s from %d\n", ret, users[connfd].buf, connfd );
/*返回-1表示出错*/
if( ret < 0 )
{
if( errno != EAGAIN )
{
close( connfd );
users[fds[i].fd] = users[fds[user_counter].fd];
fds[i] = fds[user_counter];
i--;
user_counter--;
}
}
else if( ret == 0 )
{
printf( "code should not come to here\n" );
}
else
{
for( int j = 1; j <= user_counter; ++j )
{
if( fds[j].fd == connfd )
{
continue;
}
fds[j].events |= ~POLLIN;
fds[j].events |= POLLOUT;
users[fds[j].fd].write_buf = users[connfd].buf;
}
}
}
else if( fds[i].revents & POLLOUT )
{
int connfd = fds[i].fd;
if( ! users[connfd].write_buf )
{
continue;
}
ret = send( connfd, users[connfd].write_buf, strlen( users[connfd].write_buf ), 0 );
users[connfd].write_buf = NULL;
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
delete [] users;
close( listenfd );
return 0;
}
9.7 I/O复用的高级应用三:同时处理TCP和UDP服务
10. 信号
[信号使用] blog.csdn.net/m0_49476241...
10.1 Linux信号概述
10.1.1 发送信号
Linux下,一个进程给其它进程发送信号的API是kill函数。
arduino
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
该函数把信号sig发送给目标进程pid。pid可能取值如下:
PID参数 | 含义 |
---|---|
pid > 0 | 信号发送给PID为pid的进程 |
pid = 0 | 信号发送给本进程组内的其它进程 |
pid = -1 | 信号发送给除init进程外的所有进程,但需要发送者有相应权限 |
pid < -1 | 信号发送给组ID为-pid的进程组中的所有成员 |
- Linux中信号值大于0,如果sig为0,则kill函数不发送任何信号
10.1.2 信号处理方式
目标进程在收到信号时,需要定义一个接收函数来处理。信号处理函数的原型如下:
arduino
#include <signal.h>
/*使用typedef定义了一个函数指针类型,它指向一个接收int型参数返回void的函数*/
typedef void(* __sighandler_t) (int);
信号处理函数应该是可重入的,否则容易引发竞态条件。
除了用户自定义信号处理函数外,还定义了信号的两种其它处理方式。
arduino
#include <bits/signum.h>
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
-
SIG_IGN表示忽略目标信号;
-
SIG_DFL表示使用信号的默认处理方式,默认处理方式有以下几种:
- 结束进程(Term)
- 忽略信号(Ign)
- 结束进程并生成核心转储文件(Core)
- 暂停进程(Stop)
- 继续进程(Cont)
10.1.3 Linux信号
Linux的可用信号都定义在bits/signum.h头文件中,仅仅介绍几个与网络编程相关的:
信号 | 起源 | 默认行为 | 含义 |
---|---|---|---|
SIGHUP | POSIX | Term | 控制终端挂起 |
SIGCHLD | POSIX | Ign | 子进程状态发生变化(退出或暂停) |
10.1.4 中断系统调用
如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置成EINTR。我们可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
10.2 信号函数
10.2.1 signal系统调用
设置信号处理函数,可以使用下面的signal系统调用:
arduino
#include <signal.h>
_sighandler_t signal(int sig, _sighandler_t _handler);
- sig参数指出捕获的信号类型;
- _handler参数是 _sighandler_t类型的函数指针,指定信号sig的处理函数。
signal系统调用出错时返回SIG_ERR,并设置errno。
10.2.2 sigaction系统调用
设置信号处理函数的更健壮的接口是如下的系统调用:
arduino
#include <signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);
-
sig参数指出要捕获的信号类型;
-
act参数指定新的信号处理方式;
-
oact参数输出先前的处理方式。
-
act和oact都是sigaction结构体类型的指针,定义如下:
cstruct sigaction { #ifdef _USE_POSIX199309 union { _sighandler_t sa_handler; void (*sa_sigaction) (int, siginfo_t*, void*) } _sigaction_handler; /*定义类型和变量*/ #define sa_handler __sigaction_handler.sa_handler #define sa_sigaction __sigaction_handler.sa_sigaction #else _sighandler_t sa_handler; #endif _sigset_t sa_mask; int sa_flags; void (*sa_restorer) (void); }
- sa_handler指定信号处理函数;
- sa_mask成员设置进程的信号掩码,以指定哪些信号不能发送给本进程;
- sa_flags用于设置程序收到信号时的行为。
-
sig_action成功时返回0,失败时候返回-1并设置errno。
10.3 信号集
10.3.1 信号集函数
LInux使用数据结构sigset_t来表示一组信号。定义如下:
arduino
#include <bits/sigset.h>
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
typedef struct {
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
可见,sigset_t是一个长整型数组,每个位表示一个信号。这种定义方式和文件描述符集fd_set类似。 Linux提供了如下一组函数来设置、修改、删除和查询信号集:
arduino
#include <signal.h>
int sigemptyset(sigset_t* _set) /*清空信号集*/
int sigfillset(sigset_t* _set) /*为信号集添加所有信号*/
int sigaddset(sigset_t* _set, int _signo) /*将信号_signo添加到信号集中*/
int sigdelset(sigset_t* _set, int _signo) /*将信号_signo从信号集中删除*/
int sigismember(_const sigset_t* _set, int _signo) /*测试_signo是否在信号集中*/
10.3.2 进程信号掩码
我们可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码。如下函数也可以用于设置或查看进程的信号掩码:
arduino
#include <signal.h>
int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset);
-
_set参数指定新的信号掩码;
-
_oset参数则输出原来的信号掩码;
-
_how参数指定设置进程信号掩码的方式,可选如下表:
_HOW参数 含义 SIG_BLOCK 新的进程信号掩码是当前值和_set指定信号集的并集 SIG_UNBLOCK 新的进程信号掩码是当前值和 !_set 信号集的交集 SIG_SETMASK 直接将进程信号掩码设置为_set
如果_set为NULL,则进程信号掩码不变,此时仍可以利用 _oset参数获得进程当前的信号掩码。
10.3.3 被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号 。如果取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:
arduino
#include <signal.h>
int sigpending(sigset_t* set);
- set参数用于保存被挂起的信号集
10.4 统一事件源
通过把信号的主要处理逻辑放到主循环中,当信号处理函数被触发时,它只是简单的把信号传递给主程序。信号处理函数通常通过管道来传输信号给主循环。我们可以使用I/O多路复用来监听信号的到来。这就是统一信号源。
ini
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
static int pipefd[2];
int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
void addfd( int epollfd, int fd )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
setnonblocking( fd );
}
void sig_handler( int sig )
{
int save_errno = errno;
int msg = sig;
send( pipefd[1], ( char* )&msg, 1, 0 );
errno = save_errno;
}
/*
为什么sigaction sa是一个局部变量,或者说为什么一个信号拥有一个sigset?
*/
void addsig( int sig )
{
struct sigaction sa;
memset( &sa, '\0', sizeof( sa ) );
/*设置信号处理函数和处理方式*/
sa.sa_handler = sig_handler;
/*重新调用被该信号终止的系统调用*/
sa.sa_flags |= SA_RESTART;
/*设置信号掩码*/
sigfillset( &sa.sa_mask );
assert( sigaction( sig, &sa, NULL ) != -1 );
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
if( ret == -1 )
{
printf( "errno is %d\n", errno );
return 1;
}
ret = listen( listenfd, 5 );
assert( ret != -1 );
/*创建内核事件表的文件描述符*/
epoll_event events[ MAX_EVENT_NUMBER ];
int epollfd = epoll_create( 5 );
assert( epollfd != -1 );
/*为监听socket注册读事件,且为ET模式*/
addfd( epollfd, listenfd );
/*
匿名管道pipe和有名管道mkfifo是单工的,而socketpair是全双工的
*/
ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd );
assert( ret != -1 );
/*下面两句,全双工管道的两侧都是非阻塞的*/
setnonblocking( pipefd[1] );
/*注册读事件,非阻塞*/
addfd( epollfd, pipefd[0] );
// add all the interesting signals here
addsig( SIGHUP );
addsig( SIGCHLD );
addsig( SIGTERM );
addsig( SIGINT );
bool stop_server = false;
while( !stop_server )
{
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
if ( ( number < 0 ) && ( errno != EINTR ) )
{
printf( "epoll failure\n" );
break;
}
for ( int i = 0; i < number; i++ )
{
int sockfd = events[i].data.fd;
if( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd );
}
else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )
{
int sig;
char signals[1024];
ret = recv( pipefd[0], signals, sizeof( signals ), 0 );
if( ret == -1 )
{
continue;
}
else if( ret == 0 )
{
continue;
}
else
{
for( int i = 0; i < ret; ++i )
{
//printf( "I caugh the signal %d\n", signals[i] );
switch( signals[i] )
{
/*子进程状态发生变化*/
case SIGCHLD:
/*控制中断挂起*/
case SIGHUP:
{
continue;
}
/*终止进程*/
case SIGTERM:
/*键盘输入以中断进程*/
case SIGINT:
{
stop_server = true;
}
}
}
}
}
else
{
}
}
}
printf( "close fds\n" );
close( listenfd );
close( pipefd[1] );
close( pipefd[0] );
return 0;
}