07 定时器处理非活动连接(上)

news2024/11/15 4:38:37

07 定时器处理非活动连接(上)

基础知识

非活跃,是指客户端(这里是浏览器)与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。
定时事件,是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源。
定时器,是指利用结构体或其他形式,将多种定时事件进行封装起来。具体的,这里只涉及一种定时事件,即定期检测非活跃连接,这里将该定时事件与连接资源封装为一个结构体定时器。
定时器容量,是指使用某种容器类数据结构,将上述多个定时器组合起来,便于对定时事件统一管理。具体的,项目中使用升序链表将所有定时器串联组织起来。

整体概述

本项目中,服务器主循环为每一个连接创建一个定时器,并对每个连接进行定时。另外,利用升序时间链表容器将所有容器串联起来,若主循环接收到定时通知,则在链表中依次执行定时任务。
Linux下提供了三种定时的方法:

  • sokcet选项SO_RECVTIMEO和SO_SNDTIMEO
  • SIGALRM信号
  • I/O复用系统调用的超时参数
    三种方法没有一劳永逸的应用场景,也没有绝对的优劣。项目中使用的是SIGALRM信号。
    利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用地资源。
    定时器处理非活动连接模块,主要分为定时方法与信号通知流程,定时器及其容器设计与定时任务地处理。

本文内容

定时方法与信号流程,具体设计到基础API、信号通知流程和代码实现。
基础API,描述sigaction结构体,sigaction函数,sigfillset函数,SIGALRM信号、SIGTERM信号、alarm函数,socketpair函数、send函数。
信号通知流程,介绍统一事件源和信号处理机制。
代码实现,结合代码对信号处理函数地设计与使用进行详解。

基础API

sigaction结构体

struct sigaction{
	void (*sa_handler)(int);
	void (*sa_sigaction)(int,siginfo_t *,void *);
	sigset_t sa_mask;
	int sa_flags;
	void (*sa_restorer)(void);
}
  • sa_handler是一个函数指针,指向信号处理函数
  • sa_sigaction同样是信号处理函数,有三个参数,可以获得关于信号更详细的信息
  • sa_mask用来指定在信号处理函数执行期间需要被屏蔽的信号
  • sa_flags用于指定信号处理的行为
    SA_RESTART,使被信号打断的系统调用自动重新发起
    SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到SIGCHLD信号
    SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵尸进程
    SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
    SA_RESETHAND,信号处理之后重新设置为默认的处理方式
    SA_SIGINFO,使用sa_sigaction成员而不是sa_handler作为信号处理函数
  • sa_restorer一般不使用

sigaction函数

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
  • signum表示操作的信号
  • act表示对信号设置新的处理方式
  • oldact表示信号原来的处理方式
  • 返回值,0表示成功,-1表示有错误发生

sigfillset函数

#include <signal.h>

int sigfillset(sigset_t *set);

用来将参数set信号集初始化,然后将所有的信号加入到此信号集里。

SIGALRM、SIGTREM信号

#define SIGALRM 14//由alarm系统调用产生timer时钟信号
#define SIGTERM 15//终端发送的终止信号

alarm函数

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。如果未设置信号SIGALRM的处理函数,那么alarm()默认终止进程。

socketpair函数

在linux下,使用socketpair函数能够创建一对套接字进行通信,项目中使用管道通信。

#include <sys/types.h>
#include <sys/socket.h>

int socketpair(int domain,int type,int protocol,int sv[2]);
  • domain表示协议族,PF_UNIX或者AF_UNIX
  • type表示协议,可以是SOCKSTREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP
  • protocol表示类型,只能未0
  • sc[2]表示套接字柄对,该两个句柄作用相同,均能进行读写双向操作
  • 返回结果,0创建成功,-1创建失败

send函数

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd,const void *buf,size_t len,int flags);

当套接字发送缓冲区变满后,send通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回EAGAIN或者EWOULDBLOCK错误,此时可以调用select函数来监视何时可以发送数据。

信号通知流程

Linux下的信号采用的异步处理机制,信号处理函数和当前进程是两条不同的执行路线。具体的,当进程收到信号时,操作系统会中断当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行。
为避免信号竞态现象发生,信号处理期间系统不会再次触发它。所以,为确保该信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕。
一般的信号处理函数需要处理该信号对应的逻辑,当逻辑比较复杂时,信号处理函数执行时间过长,会导致信号屏蔽太久。
这里的解决方案是,信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码。

统一事件源

统一事件源,是指将信号事件与其他事件一样处理。
信号处理函数使用管道将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用I/O复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来检测,从而实现统一处理。

信号处理机制

每个进程之中,都有存着一个表,里面存着每种信号所代表的含义,内核通过设置表项中每一个位来标识对应的信号类型。
在这里插入图片描述
信号的接收

  • 接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。此时信号还只是在队列中,对进程来说暂时是不知道由信号到来的。

信号的检测

  • 进程从内核态返回到用户态前进行信号检测
  • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
  • 进程陷入内核态后,由两种场景会对信号进行检测
  • 当发现有新信号时,便会进入下一步,信号的处理

信号的处理

  • (内核)信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。
  • (用户)接下来进程返回到用户态中,执行相应的信号处理函数
  • (内核)信号处理函数执行完成后,还需要返回内核态,检查是否还有其他信号未处理
  • (用户)如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。

至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。

代码分析

信号处理函数

自定义信号处理函数,创建sigaction结构体变量,设置信号函数。

//信号处理函数
void sig_handler(int sig)
{
	//为保证函数的可重入性,保证原来的errno
	//可重入性表示中断后再次进入该函数,环境变量与之前相同,不会丢失数据
	int save_errno=errno;
	int msg=sig;

	//将信号值从管道写端写入,传输字符类型,而非整型
	send(pipefd[1],(char *)&msg,1,0);

	//将原来的errno赋值为当前的errno
	errno=save_errno;
}

信号处理函数中仅仅通过管道发送信号值,不处理信号对应的逻辑,缩短异步执行时间,减少对主程序的影响。

//设置信号函数
void addsig(int sig,void(handler)(int),bool restart=true)
{
	//创建sigaction结构体变量
	struct sigaction sa;
	memset(&sa,'\0',sizeof(sa));

	//信号处理函数中仅仅发送信号值,不做对应逻辑处理
	sa.handler=handler;
	if(restart)
		sa.sa_flags|=SA_RESTART;
	//将所有信号添加到信号集中
	sigfillset(&sa.sa_mask);

	//执行sigaction函数
	assert(sigaction(sig,&sa,NULL)!=-1);
}

项目中设置信号函数,仅关注SIGALRM和SIGTERM两个信号。

信号通知逻辑

  • 创建管道,其中管道写端写入信号值,管道读端通知I/O复用系统监测读事件
  • 设置信号处理函数SIGALRM(时间到了触发)和SIGTERM(kill会触发,Ctrl+C)
    通过struct sigaction结构体和sigaction函数注册信号捕捉函数
    在结构体的handler参数设置信号处理函数,具体的,从管道写端写入信号的名字
  • 利用I/O复用系统监听管道读端文件描述符的可读事件
  • 信号值传递给主循环,主循环再根据收到的信号值执行目标信号对应的逻辑代码

代码分析

//创建管道套接字
ret=socketpair(PF_UNIX,SOCK_STREAM,0,pipefd);
assert(ret!=-1);

//设置管道写端为非阻塞,为什么写端要非阻塞?
setnonblocking(pipefd[1]);

//设置管道读端为ET非阻塞
addfd(epollfd,pipefd[0],false);

//传递给主循环的信号值,这里只关注SIGALRM和SIGTERM
addsig(SIGALRM,sig_handler,false);
addsig(SIGTERM,sig_handler,false);

//循环条件
bool stop_server=false;

//c超时标志
bool timeout=false;

//每隔TIMESLOT时间触发SIGALRM信号
alarm(TIMESLOT);

while(!stop_server)
{
	//监测发生事件的文件描述符
	int number=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
	if(number<0&&errno!=EINTR)
	{
		break;
	}

	//轮询文件描述符
	for(int i=0;i<number;i++)
	{
		int sockfd=events[i].data.fd;

		//管道读端对应文件描述符发生读事件
		if((sockfd==pipefd[0])&&(events[i].events&EPOLLIN))
		{
			int sig;
			char signals[1024];

			//从管道读端读出信号值,成功返回字节数,失败返回-1
			//正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
			ret=recv(pipefd[0],signals,sizeof(signals),0);
			if(ret==-1)
			{
				//handler the error
				continue;
			}
			else if(ret==0)
			{
				continue;
			}
			else
			{
				//处理信号值对应的逻辑
				for(int i=0;i<ret;++i)
				{
					//这里面是字符
					switch(signals[i])
					{
					//这里是整型
					case SIGALRM:
					{
						timeout=true;
						break;
					}
					case SIGTERM:
					{
						stop_server=true;
					}
					}
				}
			}
		}
	}
}

为什么管道写端要非阻塞?
send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,这时候会进一步增加信号处理函数的执行事件,为此,将其修改为非阻塞。
没有对非阻塞返回值处理,如果阻塞是不是意味着这一次定时事件失效了?
是的,但定时事件是非必须立即处理的事件,可以允许这样的情况发生。
管道传递的是什么类型?switch-case的变量冲突?
信号本身是整型数值,管道中传递的是ASCII码表中整型数值对应的字符。
switch的变量一般为字符或整型,当switch的变量为字符时,case可以是字符,也可以是字符对应的ASCII码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/817852.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

kotlin 编写一个简单的天气预报app(三)broadcast换成eventbus

使用eventbus替换broadcast 将从Broadcast切换到EventBus有以下几个好处&#xff1a; 解耦性&#xff1a;通过使用EventBus&#xff0c;您可以实现组件之间的解耦。传统的Broadcast机制需要发送方和接收方明确知道对方的存在&#xff0c;并且需要在代码中设置Intent过滤器和广…

c++ | 动态链接库 | 小结

//环境 linux c //生成动态链接库 //然后调用动态链接库中的函数//出现的问题以及解决//注意在win和在linux中调用动态链接库的函数是不一样的//在要生成链接库的cpp文件中比如以后要调用本文件中的某个函数&#xff0c;需要extern "c" 把你定的函数“再封装”避免重…

java实现日期拆分的方法

java实现日期拆分的方法 本文实例讲述了java实现日期拆分的方法。分享给大家供大家参考。具体如下&#xff1a; 如&#xff1a;计算6-1至6-5之间的日期天数及具体日期&#xff0c;预期的结果是得到&#xff1a; 6-1 6-2 6-3 6-4 6-5 以下是我利用java 日历类做的实现&am…

XtarBackup 8.0.33-28 prepare 速度提升 20 倍!

在这篇博文中&#xff0c;我们将描述 Percona XtraBackup 8.0.33-28 的改进&#xff0c;这显著减少了备份准备所需的时间&#xff0c;以便进行恢复操作。 Percona XtraBackup 中的这一改进显着缩短了新节点加入 Percona XtraDB 集群&#xff08;PXC&#xff09; 所需的时间。 …

多模光模块中Lens透镜的关键作用

随着现代通信技术的飞速发展&#xff0c;多模光模块已经成为光通信系统中不可或缺的关键组件。这些模块可实现高速、高容量的数据传输&#xff0c;广泛应用于数据中心、局域网和广域网等领域。在多模光模块中&#xff0c;透镜作为其中的重要组成部分&#xff0c;扮演着至关重要…

一些网络知识总结(自用)

一些网络知识总结&#xff08;自用&#xff09; 1. 进制的转换 所有进制转换成十进制就是把字面值*权数 比如16进制 5AEF 转成10机制话就是 1* 151614321064 * 5 2. ip地址网段的概念&#xff0c;可用ip&#xff0c;广播地址 比如一个ip为10.1.1.1/24那么他的网络号就是前…

C++继承特性(3)——类的默认成员函数

目录 前情回顾&#xff1a; 一.构造函数和析构函数&#xff1a; 情况1&#xff1a;子类没有写构造和析构函数时&#xff1a; 运行结果&#xff1a; 构造函数&#xff1a; 析构函数&#xff1a; 情况2&#xff1a;父类的构造函数并没有为成员变量初始化赋值&#xff0c;而…

论文阅读-BotPercent: Estimating Twitter Bot Populations from Groups to Crowds

目录 摘要 引言 方法 数据集 BotPercent架构 实验结果 活跃用户中的Bot数量 Bot Population among Comment Sections Bot Participation in Content Moderation Votes Bot Population in Different Countries’ Politics 论文链接&#xff1a;https://arxiv.org/pdf/23…

BES2700 SDK绝对时间获取方法

1 代码 2 实验 log 需要换算下

CLIP-GCD: Simple Language Guided Generalized Category Discovery(论文翻译)

CLIP-GCD: Simple Language Guided Generalized Category Discovery 摘要1 介绍2 相关工作2.1 NCD2.2 无监督聚类2.3 自监督和多模态预训练 3 方法3.1 GCD 问题设置3.2 我们的方法3.2.1 使用CLIP 在GCD 4 实验4.1 模型架构细节4.2 数据集和评估4.3 和最先进水平比较4.4 分析4.5…

echarts柱状图横坐标文字过长的解决办法

背景&#xff1a;echarts图中横坐标显示的文字过长&#xff0c;导致字都堆积在一块如下图所示 解决办法 一&#xff1a;可以尝试修改‘axisLabel’的‘rotate’和‘interval’参数&#xff0c;‘rotate’参数可以设置标签的旋转角度&#xff0c;可以避免标签之间的重叠&#x…

9.环境对象和回调函数

9.1环境对象 指的是函数内部特殊的变量this&#xff0c;它代表着当前函数运行时所处的环境 作用&#xff1a; 弄清楚this的指向&#xff0c;可以让我们代码更简洁 ➢函数的调用方式不同&#xff0c;this指代的对象也不同 ➢[谁调用&#xff0c;this 就指代谁] 是判断this指向的…

三十一章 uboot顶层Makefile详解

编译后的uboot源码文件 1、 arch 文件夹 这个文件夹里面存放着和架构有关的文件&#xff0c;进入后打开arm文件 2、 board 文件夹 board 文件夹就是和具体的板子有关的&#xff0c;打开此文件夹&#xff0c;里面全是不同的板子&#xff0c;毫无疑问正 点原子的开发板肯定也在…

如何使用Postman创建Mock Server?

这篇文章将教会大家如何利用 Postman&#xff0c;通过 Mock 的方式测试我们的 API。 什么是 Mock Mock 是一项特殊的测试技巧&#xff0c;可以在没有依赖项的情况下进行单元测试。通常情况下&#xff0c;Mock 与其他方法的主要区别就是&#xff0c;用于取代代码依赖项的模拟对…

python与深度学习(十二):CNN和猫狗大战二

目录 1. 说明2. 猫狗大战的CNN模型测试2.1 导入相关库2.2 加载模型2.3 设置保存图片的路径2.4 加载图片2.5 图片预处理2.6 对图片进行预测2.7 显示图片 3. 完整代码和显示结果4. 多张图片进行测试的完整代码以及结果 1. 说明 本篇文章是对上篇文章猫狗大战训练的模型进行测试。…

【构造】CF1758 D

Problem - D - Codeforces 题意&#xff1a; 思路&#xff1a; 如果需要构造一个和为定值的序列&#xff0c;那么考虑n-d,n-d1,.....nd-1,nd这种形式 如果要保证不能重复&#xff0c;那么先考虑一个排列&#xff0c;然后在排列上操作 如果根据小数据构造出了一些简单情形&a…

给初学嵌入式的菜鸟一点建议.学习嵌入式linux

学习嵌入式&#xff0c;我认为两个重点&#xff0c;cpu和操作系统&#xff0c;目前市场是比较流行arm&#xff0c;所以推荐大家学习arm。操作系统很多&#xff0c;我个人对开始学习的人&#xff0c;特别不是计算机专业的&#xff0c;推荐学习ucos。那是开源的&#xff0c;同时很…

CSDN 一周年创作纪念日(PS:vnjohn)

&#x1f52d; 嗨&#xff0c;您好 &#x1f44b; 我是 vnjohn&#xff0c;在互联网企业担任 Java 开发&#xff0c;CSDN 优质创作者 &#x1f4d6; 推荐专栏&#xff1a;Spring、MySQL、Nacos、Java&#xff0c;后续其他专栏会持续优化更新迭代 &#x1f332;文章所在专栏&…

【方法】PDF可以转换成Word文档吗?如何操作?

很多人喜欢在工作中使用PDF&#xff0c;因为PDF格式可以准确地保留文档的原始格式&#xff0c;比如字体、图像、布局和颜色等。 但如果编辑文档的话&#xff0c;PDF还是没有Word文档方便。那可以将PDF转换成Word格式&#xff0c;再来编辑吗&#xff1f;如何操作呢&#xff1f;…

HttpRunner自动化工具之实现参数化传递

参数化实现及重复执行 参数化测试&#xff1a;在接口测试中&#xff0c;为了实现不同组数据对同一个功能模块进行测试&#xff0c;需要准备多组测试数据对模块进行测试的过程。 在httprunner中可以通过如下方式实现参数化&#xff1a; 1、在YAML/JSON 中直接指定参数列表 2、…