Linux网络编程
代码 | 名字 | 说明 |
0 | Success | |
1 | EPERM | Operation not permitted |
2 | ENOENT | No such file or directory |
3 | ESRCH | No such process |
4 | EINTR | Interrupted system call |
5 | EIO | Input/output error |
6 | ENXIO | No such device or address |
7 | E2BIG | Argument list too long |
8 | ENOEXEC | Exec format error |
9 | EBADF | Bad file descriptor |
10 | ECHILD | No child processes |
11 | EAGAIN |
Try again / Resource temporarily unavailable 提示操作将被阻塞,但是请求了非阻塞操作 对于send(),可能原因包括:
|
12 | ENOMEM | Cannot allocate memory |
13 | EACCES | Permission denied |
14 | EFAULT | Bad address |
15 | ENOTBLK | Block device required |
16 | EBUSY | Device or resource busy |
17 | EEXIST | File exists |
18 | EXDEV | Invalid cross-device link |
19 | ENODEV | No such device |
20 | ENOTDIR | Not a directory |
21 | EISDIR | Is a directory |
22 | EINVAL | Invalid argument |
23 | ENFILE | Too many open files in system |
24 | EMFILE | Too many open files |
25 | ENOTTY | Inappropriate ioctl for device |
26 | ETXTBSY | Text file busy |
27 | EFBIG | File too large |
28 | ENOSPC | No space left on device |
29 | ESPIPE | Illegal seek |
30 | EROFS | Read-only file system |
31 | EMLINK | Too many links |
32 | EPIPE | Broken pipe |
33 | EDOM | Numerical argument out of domain |
34 | ERANGE | Numerical result out of range |
35 | Resource deadlock avoided | |
36 | File name too long | |
37 | No locks available | |
38 | Function not implemented | |
39 | Directory not empty | |
40 | Too many levels of symbolic links | |
41 | Unknown error 41 | |
42 | No message of desired type | |
43 | Identifier removed | |
44 | Channel number out of range | |
45 | Level 2 not synchronized | |
46 | Level 3 halted | |
47 | Level 3 reset | |
48 | Link number out of range | |
49 | Protocol driver not attached | |
50 | No CSI structure available | |
51 | Level 2 halted | |
52 | Invalid exchange | |
53 | Invalid request descriptor | |
54 | Exchange full | |
55 | No anode | |
56 | Invalid request code | |
57 | Invalid slot | |
58 | Unknown error 58 | |
59 | Bad font file format | |
60 | Device not a stream | |
61 | No data available | |
62 | Timer expired | |
63 | Out of streams resources | |
64 | Machine is not on the network | |
65 | Package not installed | |
66 | Object is remote | |
67 | Link has been severed | |
68 | Advertise error | |
69 | Srmount error | |
70 | Communication error on send | |
71 | Protocol error | |
72 | Multihop attempted | |
73 | RFS specific error | |
74 | Bad message | |
75 | Value too large for defined data type | |
76 | Name not unique on network | |
77 | File descriptor in bad state | |
78 | Remote address changed | |
79 | Can not access a needed shared library | |
80 | Accessing a corrupted shared library | |
81 | .lib section in a.out corrupted | |
82 | Attempting to link in too many shared libraries | |
83 | Cannot exec a shared library directly | |
84 | Invalid or incomplete multibyte or wide character | |
85 | Interrupted system call should be restarted | |
86 | Streams pipe error | |
87 | Too many users | |
88 | Socket operation on non-socket | |
89 | Destination address required | |
90 | Message too long | |
91 | Protocol wrong type for socket | |
92 | Protocol not available | |
93 | Protocol not supported | |
94 | Socket type not supported | |
95 | Operation not supported | |
96 | Protocol family not supported | |
97 | Address family not supported by protocol | |
98 | Address already in use | |
99 | Cannot assign requested address | |
100 | Network is down | |
101 | Network is unreachable | |
102 | Network dropped connection on reset | |
103 | Software caused connection abort | |
104 | Connection reset by peer | |
105 | No buffer space available | |
106 | Transport endpoint is already connected | |
107 | Transport endpoint is not connected | |
108 | Cannot send after transport endpoint shutdown | |
109 | Too many references: cannot splice | |
110 | Connection timed out | |
111 | Connection refused | |
112 | Host is down | |
113 | No route to host | |
114 | Operation already in progress | |
115 | Operation now in progress | |
116 | Stale NFS file handle | |
117 | Structure needs cleaning | |
118 | Not a XENIX named type file | |
119 | No XENIX semaphores available | |
120 | Is a named type file | |
121 | Remote I/O error | |
122 | Disk quota exceeded | |
123 | No medium found | |
124 | Wrong medium type |
选项 | 说明 |
SO_REUSEADDR |
当绑定源地址的时候,可以通过绑定到0.0.0.0:port来绑定到所有本地网络地址的对应端口上,也可以绑定到10.0.0.1:port来绑定到特定本地网络地址的端口 在默认设置下,没有socket能够绑定到同一地址的同一端口。比如在Socket A已经绑定了0.0.0.0:80以后,Socket B若是想要绑定10.0.0.0.1:80,那就会报 EADDRINUSE。因为Socket A已经绑定了所有ip地址的80端口,包括10.0.0.1:8000 如果Socket B设置了选项SO_REUSEADDR,那么:
此外SO_REUSEADDR还可以允许绑定TIME_WAIT状态的连接占据的源地址 |
SO_REUSEPORT | 允许多个Socket绑定到完全相同的IP和端口 |
Berkeley sockets(BSD sockets,来自BSD 4.2,1983),是一套用于操控因特网套接字、UNIX域套接字的接口标准,是进程间通信的一种方式。
POSIX sockets与Berkeley sockets的差别很小。大部分的现代操作系统实现了Berkeley sockets接口,甚至包括Microsoft的Winsock。
一个套接字(连接)由五元组来标识: <protocol>, <src addr>, <src port>, <dest addr>, <dest port>
其中:
- protocol在创建 socket()时设置
- src addr / src port 在 bind()时设置
- dest addr / dest port在 connect()时设置
虽然UDP不需要connect(),但是dest addr / dest port会在第一次发送数据数据时由系统隐式设置。
这套API主要包含了以下头文件:
头文件 | 说明 |
sys/socket.h | 包括BSD套接字的核心函数、数据结构 |
netinet/in.h | 与AF_INET、AF_INET6地址族相关的内容 |
sys/un.h | 与PF_UNIX/PF_LOCAL地址族相关的内容 |
arpa/inet.h | 包含一些操控IP地址的函数 |
netdb.h | 用来转换协议名称、主机名称为数字化的地址 |
unistd.h | UNIX标准头,包含很多系统调用(fork、pipe)的封装和I/O原语,例如read、write、close |
包含的主要函数有:
函数 | 说明 | ||
socket() | 创建以整数(描述符)来识别的、指定类型的套接字,并为其分配系统资源
|
||
bind() | 一般用于服务器端,将一个socket与socket address结构体(包含本地IP、端口信息)关联
|
||
listen() | 用于服务器端,导致绑定的TCP socket进入监听(LISTENING)状态
|
||
connect() | 用于客户端,将本地空闲端口分配给套接字,对于TCP,该函数将尝试建立TCP连接
|
||
accept() | 用于服务器端,接受一个入站连接请求,创建并返回关联了socket pair的socket
|
||
send() | TCP Socket的send()是一个异步调用,当数据送入socket send buffer以后就会返回。也就是说,在send()返回以后,数据仍然需要经历TCP拥塞控制等过程,才能被成功发送 | ||
recv() | read返回值:0表示对端关闭了连接;-1表示发生错误 | ||
write() | |||
read() | |||
sendto() | 用于UDP场景下的发送数据 | ||
recvfrom() | 用于UDP场景下的接收数据 | ||
close() |
关闭套接字的输入/输出通道,导致系统释放与套接字相关的资源,对于TCP,连接被关闭。对于客户端,即使connect()失败也要关闭套接字 该调用仅仅销毁了套接字的接口,套接字本身由OS内核负责销毁,某些情况下,套接字可能进入TIME_WAIT状态 |
||
shutdown() |
类似于close(),但是可以实现“半关闭”,即只关闭输出通道或者输入通道 |
||
gethostbyname() | 用于解析IPv4的主机名、IP地址
|
||
gethostbyaddr() |
|
||
select() |
轮询,等待所提供列表中一个或者多个套接字可读、可写或者有错误发生。具有以下缺点:
|
||
poll() | 与select()类似,但是描述文件描述符的方式不同,select()使用fd_set结构,而poll()使用pollfd结构 | ||
epoll() | Linux内核为处理大批量文件描述符而作了改进的poll | ||
getsockopt() | 获取某个套接字选项当前的值 | ||
setsockopt() | 设置某个套接字选项的值 |
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #define DEFAULT_PORT 1918 const char* ip2txt( struct sockaddr_in addr, char* buf ); int main( void ) { int sock_desc, client_sock, read_size; struct sockaddr_in server, client; socklen_t socklen = sizeof ( server ); char client_message[2000], ipv4_buf[INET_ADDRSTRLEN]; struct timeval timeout; timeout.tv_sec = 3; timeout.tv_usec = 0; setvbuf( stdout, NULL, _IONBF, 0 ); //创建套接字,返回一个描述符 sock_desc = socket( AF_INET, SOCK_STREAM, 0 ); if ( sock_desc == -1 ) { puts( "Failed to create socket" ); return 1; } //监听套接字结构 server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons( DEFAULT_PORT ); //绑定本地监听地址到描述符 if ( bind( sock_desc, ( struct sockaddr * ) &server, socklen ) < 0 ) { puts( "Binding failed" ); return 1; } else { printf( "Binding to local socket address: %s:%d\n", ip2txt( server, ipv4_buf ), DEFAULT_PORT ); } //开始监听,等待队列容量10 listen( sock_desc, 10 ); for ( ;; ) { //接收一个客户端连接,返回代表此套接字的描述符 client_sock = accept( sock_desc, ( struct sockaddr * ) &client, &socklen ); printf( "Connection from %s:%d accepted\n", ip2txt( client, ipv4_buf ), ntohs( client.sin_port ) ); //设置读取超时,此方法用于Linux,Cygwin无效 if ( setsockopt( client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval) ) == 0 ) { printf( "recv() timeout set to %ds\n", ( int ) timeout.tv_sec ); } //循环读取数据,该方法会立即返回 while ( ( read_size = recv( client_sock, client_message, sizeof ( client_message ), 0 ) ) > 0 ) { //把数据原样写给客户端 printf( "Received message: %s\n", client_message ); write( client_sock, client_message, strlen( client_message ) ); } //如果超时后仍然读取不到数据、或者出现其他错误,会返回-1 puts( "Error recv()." ); } return 0; } const char* ip2txt( struct sockaddr_in addr, char* buf ) { //该函数用来将网络字节序的整数转换为点号分隔的IP地址格式 return inet_ntop( AF_INET, &addr.sin_addr, buf, INET_ADDRSTRLEN ); } |
客户端代码示例:
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 32 33 34 35 |
void client() { int client_sock_desc; struct sockaddr_in server; char client_message[2000], server_message[2000]; client_sock_desc = socket( AF_INET, SOCK_STREAM, 0 ); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr( "192.168.0.90" ); server.sin_port = htons( DEFAULT_PORT ); socklen_t socklen = sizeof ( server ); //连接到服务器端 if ( connect( client_sock_desc, ( struct sockaddr* ) &server, socklen ) < 0 ) { printf( "Connect failed.\n" ); return; } while ( 1 ) { printf( "Enter message: \n" ); scanf( "%s", client_message ); //发送消息 if ( send( client_sock_desc, client_message, strlen( client_message ), 0 ) < 0 ) { printf( "Send failed.\n" ); return; } //接收回应 if ( recv( client_sock_desc, server_message, sizeof ( server_message ), 0 ) < 0 ) { printf( "Recv failed.\n" ); return; } printf( "Message from server:%s\n", server_message ); } } |
一般情况下,对于网络服务器不适用“多进程”方式处理,因为网络服务器通常需要很多内存资源,fork()却会复制这些资源。这里只是做一个简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
bind( server_sockfd, ( struct sockaddr * ) &server_address, server_len ); listen( server_sockfd, 5 ); signal( SIGCHLD, SIG_IGN ); //忽略子进程的退出 while ( 1 ) { client_len = sizeof ( client_address ); //接受一个连接请求 client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &client_address, &client_len ); //创建子进程处理请求 if ( fork() == 0 ) { //如果是子进程,读取消息并处理 //注意子进程继承父进程打开的文件描述符 read( client_sockfd, &ch, 1 ); write( client_sockfd, &ch, 1 ); close( client_sockfd ); exit( 0 ); } else { //如果是父进程,只需要关闭描述符 close( client_sockfd ); } } |
select系统调用允许同时在多个底层文件描述符上等待输入的到达或输出的完成,避免在单个输入输出上的忙等待。下面是相关的函数说明:
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 |
#include <sys/types.h> #include <sys/time.h> //下面这组宏用于对文件描述符集合进行操作 void FD_ZERO( fd_set *fdset );//将文件描述符集初始化为空集合 int FD_ISSET( int fd, fd_set *fdset ); //判断文件描述符fd是否存在于fdset中 void FD_CLR( int fd, fd_set *fdset ); //清除一个文件描述符 void FD_SET( int fd, fd_set *fdset ); //设置一个文件描述符 /** * 测试文件描述符集中是否至少有一个文件描述符已经处于可读、可写、错误状态。 * 该函数在以下情况下返回: * 1、readfds具有可读、writefds具有可写、errorfds存在描述符遇到错误条件 * 2、如果上述三个情况都没有发生,该调用在timeout后返回 * 当返回时,描述符集合被修改为指示哪些描述符可读、可写或者处于错误状态 * * @param nfds 需要测试的文件描述符数量,从0到nfds-1 * 下面三个参数都可以被设置为空指针,表示不进行相应的测试 * @param readfds 读文件描述符 * @param writefds 写文件描述符 * @param errorfds 错误文件描述符 * @param timeout 超时时间,如果是空指针将一直阻塞 * @return 状态无变化的描述符总数,失败时返回-1并设置errno: * EBADF 无效文件描述符 * EINTR 因中断而返回 * EINVAL ndfs或者timeout取值错误 */ int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout ); struct timeval { time_t tv_sec; /* 毫秒 */ long tv_usec; /* 微秒 */ }; |
下面是一段示例代码,通过单进程服务多个客户端,并将请求值加1返回给客户端:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdlib.h> #define DEFAULT_PORT 1918 int main() { int server_sockfd, client_sockfd; int server_len, client_len; struct sockaddr_in server_address; struct sockaddr_in client_address; int result; fd_set readfds, testfds; //创建客户端TCP套接字 server_sockfd = socket( AF_INET, SOCK_STREAM, 0 ); server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = htonl( INADDR_ANY ); server_address.sin_port = htons( DEFAULT_PORT ); server_len = sizeof ( server_address ); //绑定套接字描述符到本地地址 bind( server_sockfd, ( struct sockaddr * ) &server_address, server_len ); //在套接字描述符上监听 listen( server_sockfd, 5 ); FD_ZERO( &readfds ); FD_SET( server_sockfd, &readfds ); //将监听描述符添加到集合中 while ( 1 ) { char ch; int fd; int nread; testfds = readfds; //一直等待,直到输入(连接请求)到达 result = select( FD_SETSIZE, &testfds, ( fd_set * ) 0, ( fd_set * ) 0, ( struct timeval * ) 0 ); for ( fd = 0; fd < FD_SETSIZE; fd++ ) { //遍历所有就绪的可读描述符 if ( FD_ISSET( fd, &testfds ) ) { if ( fd == server_sockfd ) { //这是一个服务器监听套接字 //服务器端套接字就绪,说明有连接请求 client_len = sizeof ( client_address ); //生成客户套接字,并加入到读套接字集中 client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &client_address, &client_len ); FD_SET( client_sockfd, &readfds ); } else { //这是一个客户套接字 ioctl( fd, FIONREAD, &nread ); //得到可读数据字节数 if ( nread == 0 ) { close( fd ); FD_CLR( fd, &readfds ); //从读集合中移除该客户端 } else { read( fd, &ch, 1 ); //读取一个字节 ch++; write( fd, &ch, 1 ); //写回一个字节 } } } } } } |
1 2 3 4 5 6 7 8 9 |
sockfd = socket( AF_INET, SOCK_DGRAM, 0 ); address.sin_family = AF_INET; address.sin_port = servinfo->s_port; address.sin_addr = *( struct in_addr * ) *hostinfo->h_addr_list; len = sizeof(address); //向address发送一个数据报 sendto( sockfd, buffer, 1, 0, ( struct sockaddr * ) &address, len ); //从address接收数据报 recvfrom( sockfd, buffer, sizeof(buffer), 0, ( struct sockaddr * ) &address, &len ); |
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include #include #include #include #include #include <sys/types.h> #include int main(int argc, char *argv[]) { int listenfd = 0, connfd = 0; struct sockaddr_in serv_addr; char sendBuff[1025]; time_t ticks; // 下面的调用在内核中创建一个未命名套接字,返回一个整数 —— 套接字描述符 // AF_INET 地址族,对于IPv4使用AF_INET // SOCK_STREAM 传输层协议类型,流式表示需要确认机制 // 0 让内核决定默认协议,AF_INET + SOCK_STREAM -> TCP listenfd = socket(AF_INET, SOCK_STREAM, 0); memset(&serv_addr, '0', sizeof(serv_addr)); memset(sendBuff, '0', sizeof(sendBuff)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(5000); // 将serv_addr所指定的细节信息,绑定到listenfd。bind调用时可选的,如果不调用,内核可以随机选取监听端口 bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 在此套接字上监听新连接请求,最多10个排队 listen(listenfd, 10); while(1) { // accept导致当前线程休眠,当有新的客户端请求进入,并且完成3次握手后,线程醒来 // 获得代表客户端套接字的套接字描述符 connfd = accept(listenfd, (struct sockaddr*)NULL, NULL); ticks = time(NULL); snprintf(sendBuff, sizeof(sendBuff), "%.24s\r\n", ctime(&ticks)); // 可以向套接字描述符中写数据 write(connfd, sendBuff, strlen(sendBuff)); // 关闭描述符 close(connfd); sleep(1); } } |
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include #include #include #include #include #include #include <arpa/inet.h> int main(int argc, char *argv[]) { int sockfd = 0, n = 0; char recvBuff[1024]; struct sockaddr_in serv_addr; if(argc != 2) { printf("\n Usage: %s \n",argv[0]); return 1; } memset(recvBuff, '0',sizeof(recvBuff)); // 创建一个套接字,客户端服务器没有区别 if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("\n Error : Could not create socket \n"); return 1; } memset(&serv_addr, '0', sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(5000); if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0) { printf("\n inet_pton error occured\n"); return 1; } // 客户端套接字一般都不需要绑定,由内核自由分配一个端口就足够了 // 尝试连接到远程套接字(IP+端口) if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { printf("\n Error : Connect Failed \n"); return 1; } // 读取套接字,就像读取一个普通文件一样 while ( (n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) > 0) { recvBuff[n] = 0; if(fputs(recvBuff, stdout) == EOF) { printf("\n Error : Fputs error\n"); } } if(n < 0) { printf("\n Read error \n"); } return 0; } |
应用程序周期性的调用read来检查I/O状态,以完成数据的读取,性能最差。
维护一个最长1024的数组,保存文件描述符的状态,一次select可以遍历很多文件描述符。
1 |
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
select 函数监视的文件描述符分 3 类,分别是 writefds、readfds 和 exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可)。当 select 函数返回后,通过遍历 fd_set,来找到就绪的描述符。
select 目前几乎在所有的平台上支持,其良好跨平台支持是它的一大优点。select 的一个缺点在于单个进程(Apache使用多进程方式解决此问题,尽管Linux进程很轻量,也是有代价的。而且进程间数据同步远比不上线程间同步的高效)能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
类似于select,但是使用链表解决1024数组长度限制。
1 2 3 4 5 6 |
int poll (struct pollfd *fds, unsigned int nfds, int timeout); struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ }; |
pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select参数-值传递的方式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。
从上面看,select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的 socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
效率最高,调用epoll后,如果没有发现可用的I/O事件,调用线程将会自动休眠,直到有事件发生后,内核将其唤醒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 创建 epoll 文件描述符,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议 int epoll_create(int size); // 对指定描述符 fd 执行 op 操作控制,event 是与 fd 关联的监听事件 // op有三种:添加 EPOLL_CTL_ADD,删除 EPOLL_CTL_DEL,修改 EPOLL_CTL_MOD // 分别添加、删除和修改对 fd 的监听事件 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; // 等待 epfd 上的 IO 事件,最多返回 maxevents 个事件 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); |
在 select/poll 中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而 epoll 事先通过 epoll_ctl() 来注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个文件描述符,当进程调用 epoll_wait 时便得到通知
epoll 的优点主要是一下几个方面:
- 监视的描述符数量不受限制,它所支持的 fd 上限是最大可以打开文件的数目,这个数字一般远大于 2048, 举个例子, 在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看, 一般来说这个数目和系统内存关系很大
- IO 的效率不会随着监视 fd 的数量的增长而下降。epoll 不同于 select 和 poll 轮询的方式,而是通过每个 fd 定义的回调函数来实现的。只有就绪的 fd 才会执行回调函数
- 支持水平触发和边沿触发两种模式:
- 水平触发模式,文件描述符状态发生变化后,如果没有采取行动,它将后面反复通知,这种情况下编程相对简单,libevent 等开源库很多都是使用的这种模式
- 边沿触发模式,只告诉进程哪些文件描述符刚刚变为就绪状态,只说一遍,如果没有采取行动,那么它将不会再次告知。理论上边缘触发的性能要更高一些,但是代码实现相当复杂(Nginx 使用的边缘触发)
- mmap 加速内核与用户空间的信息传递。epoll 是通过内核与用户空间 mmap 同一块内存,避免了无谓的内存拷贝
Leave a Reply