程序员的知识教程库

网站首页 > 教程分享 正文

Linux网络编程:统一事件源的实现(linux 事件)

henian88 2024-10-18 06:01:37 教程分享 38 ℃ 0 评论

信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)太久。

一种典型的解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。

信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数向管道的写端写入信号值,主循环则从管道的读端读出该信号值。

那么主循环怎么知道管道上何时有数据可读呢?这很简单,我们只需要使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源。如下图所示:

很多优秀的 I/O 框架库和后台服务器程序都统一处理信号和 I/O 事件,比如 Libevent I/O 框架库和 xinetd 超级服务。

下面我们给出了统一事件源的一个简单实现,程序只是简单的读取 Ctrl + C 信号,将其信号值打印屏幕上,并结束程序。但实际中,我们可以根据需要,将不同的信号指向不同的处理逻辑。

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#define MAXFD 10
// 创建管道描述符数组pipefd
int pipefd[2];
/* 信号处理函数:将信号值写入管道的写端 */
void fun(int sig)
{
	write(pipefd[1], &sig, sizeof(sig));
}
/* 将文件描述符设置为非阻塞 */
void setnonblock(int fd)
{
	int oldfl = fcntl(fd, F_GETFL);
	int newfl = oldfl | O_NONBLOCK;
	if (fcntl(fd, F_SETFL, newfl) == -1)
	{
		perror("fcntl error");
	}
}
/* 向内核事件表epfd 中添加 新事件的文件描述符fd */
void epoll_add(int epfd, int fd)
{
	/* 设置epoll_event的结构成员 */
	struct epoll_event ev;
	ev.events = EPOLLIN | EPOLLET;
	ev.data.fd = fd;
	/* EPOLL_CTL_ADD添加新事件及描述符到内核事件表 */
	if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
	{
		perror("epoll_ctl add error\n");
	}
	setnonblock(fd);
}
void epoll_del(int epfd, int fd)
{
	/* 从内核事件表中移除fd */
	if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
	{
		perror("epoll_ctl del error\n");
	}
}
int create_sockfd();
int main()
{
	int sockfd = create_sockfd();
	assert(sockfd != -1);
	/* 创建管道 */
	pipe(pipefd);
	/* 创建内核事件表 */
	int epfd = epoll_create(MAXFD);
	assert(epfd != -1);
	epoll_add(epfd, sockfd);
	/* 添加管道的读端到内核事件表 */
	epoll_add(epfd, pipefd[0]);
	signal(SIGINT, fun);
	/* events数组存放就绪描述符 */
	struct epoll_event events[MAXFD];
	int run = 1;
	while (run)
	{
		/*
		** epoll_wait返回的是前n个已经全就绪,
		** 那么我们不用全部遍历,只遍历前n个就可以
		** 超时时间设置为5秒
		*/
		int n = epoll_wait(epfd, events, MAXFD, 5000);
		/* epoll_wait 调用失败 */
		if (n == -1)
		{
			if (errno != EINTR)
			{
				perror("epoll wait error!\n");
			}
		}
		else if (n == 0)
		{
			printf("time out!\n");
		}
		/* 只遍历前n个,因为内核已告诉我们前n个有就绪事件 */
		else
		{
			int i = 0;
			for (; i < n; ++i)
			{
				int fd = events[i].data.fd;
				if (fd == -1)
				{
					continue;
				}
				/* events 为内核为我们返回的就绪事件 */
				if (events[i].events & EPOLLIN)
				{
					if (fd == sockfd)
					{
						struct sockaddr_in caddr;
						int len = sizeof(caddr);
						/* 接收一个套接字已建立的连接,得到连接套接字connfd */
						int connfd = accept(sockfd, (struct sockaddr*) & caddr, (socklen_t*)& len);
						if (connfd < 0)
						{
							continue;
						}
						printf("connfd : %d\n", connfd);
						/* 将连接套接字connfd,添加到内核事件表中 */
						epoll_add(epfd, connfd);
					}
					/* 若是管道的读端就绪,说明有信号产生,打印信号值 */
					else if (fd == pipefd[0])
					{
						int sig = 0;
						read(pipefd[0], &sig, sizeof(sig));
						printf("sig = %d\n", sig);
						run = 0;
					}
					else
					{ /* 拿到一个就绪的文件描述符后,循环读取 */
						while (1)
						{
							char buff[128] = { 0 };
							/* recv用来接收客端数据 */
							int res = recv(fd, buff, 1, 0);
							if (res == 0)
							{
								/*
								** 注意:这里不能先close,应该先调用epoll_del,
								** 因为先调用close关闭了文件描述符后,再调用epoll_del
								** 内核将不能找到所要从内核事件表中移除的文件描述符
								**/
								epoll_del(epfd, fd);
								close(fd);
								printf("one client over\n");
								continue;
							}
							/* 如果返回-1,则数据已被读取完毕,结束 */
							else if (res == -1)
							{
								send(fd, "OK", 2, 0);
								break;
							}
							else
							{
								printf("buff %d = %s\n", fd, buff);
							}
						}
					}
				}
			}
		}
	}
	close(sockfd);
	close(epfd);
	printf("main over!\n");
}
int create_sockfd()
{
	/* 创建监听套接字(socket描述符),指定协议族ipv4,字节流服务传输 */
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1)
	{
		return -1;
	}
	/* socket专用地址信息设置 */
	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");
	/* 命名套接字,将socket专用地址绑定到socket描述符上 */
	int res = bind(sockfd, (struct sockaddr*) & saddr, sizeof(saddr));
	if (res == -1)
	{
		return -1;
	}
	/* 创建监听队列 */
	listen(sockfd, 5);
	return sockfd;
}

运行上述程序,当我们按下 ctrl + c 后,可以看到程序打印出了信号值,并结束。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表