I/O复用 select系统调用

本文详细探讨select系统调用在I/O多路复用中的作用,包括其API定义、文件描述符的就绪条件、select原理和代码实现。通过实例说明如何使用select监控网络套接字的读写状态,提升网络应用的性能。
摘要由CSDN通过智能技术生成

select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。

select API

select 系统调用的定义如下:

#include <sys/select.h>

int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct ti
meval *timeout);

select 成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select 将返回 0。select 失败是返回-1.如果在 select 等待期间,程序接收到信号,则 select 立即返回-1,并设置 errno 为 EINTR。

maxfd 参数指定的被监听的文件描述符的总数。它通常被设置为 select 监听的所有文件描述符中的最大值+1。
readfds、writefds 和 exceptfds 参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件描述符。select 返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
fd_set 结构如下:

#define __FD_SETSIZE 1024
typedef long int __fd_mask;
#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 结构中的位:

void FD_ZERO(fd_set *fdset); // 清除 fdset 的所有位
void FD_SET(int fd, fd_set *fdset); // 设置 fdset 的位 fd
void FD_CLR(int fd, fd_set *fdset); // 清除 fdset 的位 fd
int FD_ISSET(int fd, fd_set *fdset);// 测试 fdset 的位 fd 是否被设置

timeout 参数用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。timeval结构的定义如下:

struct timeval
{
	long tv_sec; //秒数
	long tv_usec; // 微秒数
};

如果给 timeout 的两个成员都是 0,则 select 将立即返回。如果 timeout 传递NULL,则 select 将一直阻塞,直到某个文件描述符就绪。

文件描述符的就绪条件
哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键。在网络编程中,下列情况下socket可读:

  • socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT,此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
  • socket通信的对方关闭连接。此时对该socket的读操作将返回0。
  • 监听socket上有新的连接请求。
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

下列情况下socket可写:

  • socket内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
  • socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
  • socket使用非阻塞connect连接成功或者失败(超时)之后。
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

网络程序中,select 能处理的异常情况只有一种:socket 上接收到带外数据(紧急数据)。

select 原理分析

由上面的定义可见,fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
要把关注的文件描述符集合添加到fd_set集合中可以这样处理:

int SetFdToFdset(fd_set *fdset, int fds[], int maxfd)
{
	FD_ZERO(fdset);
	int i = 0, n = fds[0];
	for (; i < maxfd; ++i)
	{
		if (fds[i] != -1)
		{
			FD_SET(fds[i], fdset);
			if (fds[i] > n)
			{
				n = fds[i];
			}
		}
	}
	return n;
}

fdset参数是要添加到达fdset集合的指针,fds是要添加的文件描述符数组,n是数组的长度。

用户通过readfds、writefds 和 exceptfds 三个参数分别传入感兴趣的可读、可写和异常事件,内核通过对这三个参数的在线修改来反馈其中的就绪事件

select采用轮询方式来检测fd_set结构体的数据位,当数据位由0变1时,所在位注册的文件描述符关注的事件就绪。

每次调用select时都要把这三个参数传给内核,且每次循环都可能修改这三个参数,所以在每次调用select之前都要重置这三个参数。

select的处理过程如下:
在这里插入图片描述

select的代码示例

使用select实现的TCP服务器代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#define MAX_FD 128
#define DATALEN 1024
// 初始化服务器端的 sockfd 套接字
int InitSocket()
{
	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;
}
// 初始化记录服务器套接字的数组
void InitFds(int fds[], int n)
{
	int i = 0;
	for (; i < n; ++i)
	{
		fds[i] = -1;
	}
}
// 将套接字描述符添加到数组中
void AddFdToFds(int fds[], int fd, int n)
{
	int i = 0;
	for (; i < n; ++i)
	{
		if (fds[i] == -1)
		{
			fds[i] = fd;
			break;
		}
	}
}
// 删除数组中的套接字描述符
void DelFdFromFds(int fds[], int fd, int n)
{
	int i = 0;
	for (; i < n; ++i)
	{
		if (fds[i] == fd)
		{
			fds[i] = -1;
			break;
		}
	}
}
//将数组中的套接字描述符设置到 fd_set 变量中,并返回当前最大的文件描述符值
int SetFdToFdset(fd_set *fdset, int fds[], int n)
{
	FD_ZERO(fdset);
	int i = 0, maxfd = fds[0];
	for (; i < n; ++i)
	{
		if (fds[i] != -1)
		{
			FD_SET(fds[i], fdset);
			if (fds[i] > maxfd)
			{
				maxfd = fds[i];
			}
		}
	}
	return maxfd;
}
void GetClientLink(int sockfd, int fds[], int n)
{
	struct sockaddr_in caddr;
	memset(&caddr, 0, sizeof(caddr));
	socklen_t len = sizeof(caddr);
	int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
	if (c < 0)
	{
		return;
	}
	printf("A client connection was successful\n");
	AddFdToFds(fds, c, n);
}
// 处理客户端数据
void DealClientData(int fds[], int n, int clifd)
{
	char data[DATALEN] = { 0 };
	int num = recv(clifd, data, DATALEN - 1, 0);
	if (num <= 0)
	{
		DelFdFromFds(fds, clifd, n);
		close(clifd);
		printf("A client disconnected\n");
	}
	else
	{
		printf("%d: %s\n", clifd, data);
		send(clifd, "OK", 2, 0);
	}
}
// 处理 select 返回的就绪事件
void DealReadyEvent(int fds[], int n, fd_set *fdset, int sockfd)
{
	int i = 0;
	for (; i < n; ++i)
	{
		if (fds[i] != -1 && FD_ISSET(fds[i],fdset))
		{
			if (fds[i] == sockfd)
			{
				GetClientLink(sockfd, fds, n);
			}
			else
			{
				DealClientData(fds, n, fds[i]);
			}
		}
	}
}
int main()
{
	int sockfd = InitSocket();
	assert(sockfd != -1);
	fd_set readfds;
	int fds[MAX_FD];
	InitFds(fds, MAX_FD);
	AddFdToFds(fds, sockfd, MAX_FD);
	while ( 1 )
	{
		int maxfd = SetFdToFdset(&readfds, fds,MAX_FD);
		struct timeval timeout;
		timeout.tv_sec = 2; // 秒数
		timeout.tv_usec = 0; //微秒数
		int n = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
		if (n < 0)
		{
			printf("select error\n");
		break;
		}
		else if (n == 0)
		{
			printf("time out\n");
			continue;
		}
		DealReadyEvent(fds, MAX_FD, &readfds, sockfd);
	}
	exit(0);
}

测试结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_200_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值