目录
select API
select系统调用的原型如下:
头文件 #include<sys/select.h>
int select(int maxfd,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout)
返回值 :select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。select失败时返回-1。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。
maxfd:参数指定的被监听的文件描述符的总数。它通常被设置为 select 监听的所
有文件描述符中的最大值+1,因为描述符是从0开始计数的。
readfds、writefds 和 exceptfds: 指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件描述符。select 返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
fd_set 结构如下:
#include <typesizes.h>
#define __FD_SETSIZE 1024
#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef __NFDBITS
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
由上面定义可见,fd_set结构体仅包含一个整形数组,该数组的每个元素的每一位(bit)标记一个文件描述。fd_set能容纳的文件描述符数量由FD_SETSZIE指定,这就限制了select能同时处理的文件描述符的总量;
由于位的操作的过于频繁,使用下面的一系列宏来访问fd_set结构体中的位:
#include <sys.select.h>
FD_ZERO ( fd_set * fdset); //清除fdset的所有位
FD_SET (int fd ,fd_set *fdset); // 设置fdset的位fd
FD_CLR (int fd ,fd_set *fdset); // 清除fdset的位fd
int FD_ISSET (int fd , fd_set *fdset); //测试fdset的位fd是否被设置
timeout参数:设置select函数的超时时间。它是一个timeval结构体类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败后timeout值是不确定的。
timeval结构体定义如下:
struct timeval{
long tv_sec; // 秒数
long tv_usec; // 微妙数
}
select提供了一个微秒级别的定时方式。如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果给timeout传递为NULL,则select将一直阻塞,直到某个文件描述符就绪。
select原理
假设用户空间 服务端监听四个socket对应的fd;
- (1)首先拷贝fd进入内核空间 ;
- (2)内核空间进行顺序检查,检查是否有就绪的fd;
- (3)内核空间将就绪的fd的数量返回用户空间;
如果内核检查没有就绪的fd;
如果内核检查没有就绪的fd,则select会阻塞,如果对应的socket有数据包到达;检查socket对应的等待队列是否有进程阻塞;有的话唤醒该进程,进程将再次检查是否有就绪的描述符;如果有就绪的fd,则会标记该fd并且返回给用户空间;
我们看以看出来 select函数返回值是int,因此返回的是文件描述符的数量;通过设置fd_set对应的数组;
select编程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/time.h>
#define FD_MAX 10
int socket_init();
void fds_init(int fds[])
{
for( int i = 0; i < FD_MAX; i++ )
{
fds[i] = -1;
}
}
void fds_add( int fds[], int fd)
{
for( int i = 0; i < FD_MAX; i++ )
{
if ( fds[i] == -1 )
{
fds[i] = fd;
break;
}
}
}
void fds_del( int fds[], int fd)
{
for( int i = 0; i < FD_MAX; i++ )
{
if( fds[i] == fd )
{
fds[i] = -1;
break;
}
}
}
int main()
{
int sockfd = socket_init();
if( sockfd == -1 )
{
printf("socket init failed\n");
exit(1);
}
int fds[FD_MAX];
fds_init(fds);
fds_add(fds,sockfd);
fd_set fdset;
while( 1 )
{
FD_ZERO(&fdset);
int maxfd = -1;
for( int i = 0; i < FD_MAX; i++ )
{
if ( fds[i] == -1 )
{
continue;
}
FD_SET(fds[i],&fdset);
if ( maxfd < fds[i] )
{
maxfd = fds[i];
}
}
struct timeval tv = {5,0};
int n = select(maxfd+1,&fdset,NULL,NULL,&tv);
if ( n == -1 )
{
continue;
}
else if ( n == 0 )
{
printf("time out\n");
continue;
}
else
{
for( int i = 0; i < FD_MAX; i++ )
{
if ( fds[i] == -1 )
{
continue;
}
if ( FD_ISSET(fds[i],&fdset) )
{
if ( fds[i] == sockfd )
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if ( c < 0 )
{
continue;
}
printf("accept c=%d\n",c);
fds_add(fds,c);
}
else
{
char buff[128] = {0};
int num = recv(fds[i],buff,127,0);
if ( num <= 0 )
{
close(fds[i]);
fds_del(fds,fds[i]);
printf("client close\n");
}
else
{
printf("recv(%d)=%s\n",fds[i],buff);
send(fds[i],"ok",2,0);
}
}
}
}
}
}
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1 )
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if ( res == -1 )
{
return -1;
}
res = listen(sockfd,5);
if ( res == -1 )
{
return -1;
}
return sockfd;
}
select总结
select将socket是否就绪检查逻辑下沉到操作系统层面,避免了系统调用,只告诉告诉用户有事件就绪,但是没有告诉用户具体那个FD;
- 优点:不需要每个FD都进行依次系统调用,解决了频繁的用户态内核态切换的问题;
- 缺点:单进程监听的FD存在限制,默认1024;(可以修改)
- 每次调用需要将FD从用用户态拷贝到内核态;
- 不知道具体的那个文件描述符就绪,需要遍历全部文件描述符;
- 入参的3个fd_set集合每次调用都要重置;