使用signal中止阻塞的socket函数的应用实例

在 socket 编程中,有一些函数是阻塞的,为了使程序高效运行,有一些办法可以把这些阻塞函数变成非阻塞的,本文介绍一种使用定时器信号中断阻塞函数的方法,同时介绍了一些信号处理和定时器设置的编程方法,本文附有完整实例的源代码,本文实例在 Ubuntu 20.04 上编译测试通过,gcc版本号为:9.4.0;本文不适合 Linux 编程的初学者阅读。

1 前言

  • 在 socket 编程中,阻塞还是不阻塞是经常要考虑的问题,accept()recv() 等一些函数都是阻塞函数,阻塞函数有时会给程序带来麻烦;
  • 使用 select() 或者 poll() 监视 socket 描述符可以有效地避免诸如 accept()recv() 等函数的阻塞带来的麻烦;
  • 下面这段代码是使用 select() 避免阻塞的示例:
    int sockfd = socket(AF_INET, SOCK_STREAM , 0);
    ......
    fd_set fds;
    FD_ZERO(fd_set);
    FD_SET(sockfd, &fds);
    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;
    if (select(sockfd + 1, &fds, NULL, NULL, &tv)) {if (FD_ISSET(sockfd, &fds)) {......}
    }
    
  • 下面这段代码是使用 poll() 避免阻塞的示例:
    int sockfd = socket(AF_INET, SOCK_STREAM , 0);
    ......
    struct pollfd pfd;
    pfd.fd = sockfd;
    pfd.events = POLLIN;
    if (poll(&pfd, 1, 5000)) {if (pfd.revents & POLLIN) {...... }
    }
    
  • 使用 ioctl() 将一个 socket 设置为非阻塞模式也是解决 socket 函数阻塞的方法之一;
  • 下面代码使用 ioctl() 将 socket 设置为非阻塞模式:
    int sockfd = socket(AF_INET, SOCK_STREAM , 0);
    int on = 1;
    ioctl(sockfd, FIONBIO, (char *)&on);
    ......
    
  • 下面这段代码使用 fcntl() 将 socket 设置为非阻塞模式,与 ioctl() 是等效的:
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    ......
    
  • 如果将 socket 设置为非阻塞模式,socket 阻塞函数将立即返回,给出一个错误代码 EAGAIN,所以代码要写成下面这样:
    int sockfd = socket(AF_INET, SOCK_STREAM , 0);
    int on = 1;
    ioctl(sockfd, FIONBIO, (char *)&on);
    ......
    int rc = 0;
    do {rc = accept(sockfd, NULL, NULL);usleep(100 * 1000);             // sleep 100 ms
    } while (rc == EAGAIN || rc == EINTR);
    ......
    
  • 本文讨论使用信号(signal)避免 socket 阻塞函数产生阻塞的方法。

2 使用 signal 中止 socket 阻塞函数

  • 实际上 socket 阻塞函数除了在非阻塞模式下会立即返回外,一旦当前进程收到信号(任何信号)时也会返回;

    • 在非阻塞模式下,socket 阻塞函数返回值为 -1 时,其 errno=EAGAIN;
    • 因为收到信号而中止的 socket 阻塞函数返回值为 -1, errno=EINTR;
  • 基于此,可以设置一个定时器,Linux 的定时器会发出一个 SIGALRM 信号,该信号显然可以中止 socket 阻塞函数的阻塞状态;

  • 设置定时器通常有两种方法,一种是使用 alarm(),另一种是使用 setitimer()

  • 下面代码使用 setitimer() 设置一个 5 秒的定时器:

    struct itimerval new_value;
    new_value.it_value.tv_sec = 5;
    new_value.it_value.tv_usec = 0;
    new_value.it_interval.tv_sec = 5;
    new_value.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &new_value, NULL);
    
  • 有关 setitimer() 的详细信息,可以查看在线手册 man setitimer,这里仅做简单介绍;

  • setitimer() 的定义:

    #include <sys/time.h>int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
    
  • 其中 struct itimeval 的定义如下:

    struct itimerval {struct timeval it_interval; /* Interval for periodic timer */struct timeval it_value;    /* Time until next expiration */
    };struct timeval {time_t      tv_sec;         /* seconds */suseconds_t tv_usec;        /* microseconds */
    };
    
  • 调用 setitimer() 时,参数 which 表示计时方式,有三个可选值:

    • ITIMER_REAL:以实际时钟计时,计时器时间到产生 SIGALRM 信号;
    • ITIMER_VIRTUAL:以进程消耗的用户模式下 CPU 时间计时,计时器时间到产生一个 SIGVTALRM 信号;
    • ITIMER_PROF:以进程消耗的总 CPU 时间计时,计时器到时时产生一个 SIGPROF 信号;
  • 调用 setitimer() 时,参数 new_value 用于设置定时器时间:

    • new_value.it_value 中有两个字段,如果两个字段均为 0,表示取消定时器,如果两个字段中有一个不为 0,则认为是设置了一个时间间隔;
    • new_value.it_interval 用于指定计时器的新间隔,当 new_value.it_interval 中的两个字段均为 0 时,表示这个计时器是单次的,其中有一个字段不为 0,则将被作为一个新的时间间隔在下次被指定;
  • 调用 setitimer() 时,参数 old_value 用于返回之前的设置值(实际就是 getitime() 返回的值),可以设置为 NULL;

  • 函数 setitimer() 调用成功时返回 0,失败时返回 -1,errno 中为错误代码;

  • alarm() 的使用比较简单,定义如下:

    #include <unistd.h>unsigned int alarm(unsigned int seconds);
    
  • alarm() 设置的时间到时,将产生一个 SIGALRM 信号,alarm() 是一个单次的定时器,所以使用 alarm() 设置的定时器只会响应一次,如果需要重复定时,可以在 SIGALRM 信号处理程序中再次执行 alarm() 重新设置定时;

  • alarm()setitimer() 使用的是同一个定时器,所以,这两个函数相互间会互相影响,建议在同一个进程中,应避免使用两种方法设置定时器;

  • alarm() 在设置定时器时只能设置到秒的精度,而且只能使用实际时钟,相比较而言,setitimer() 可以设置精度更高的定时器,而且计时方式也比较多样,但复杂度略高;

  • 不管是 alarm() 还是 setitimer(),在计时时间到时都是发出一个信号,所以编写信号处理程序是使用定时器时必须要做的工作,需要使用 signal() 设置信号处理程序;

  • 下面程序设置了 SIGALRM 信号的信号处理程序:

    void signal_handler(int sig) {signal(sig, signal_handler);printf("Catch the signal: %d\n",sig);......
    }int main() {......signal(SIGALRM, signal_handler);......
    }
    
  • signal() 函数设置的信号处理程序在信号产生后会被重置为默认处理程序,如果需要下次产生信号时继续使用当前处理程序,需要在信号处理程序中执行 signal() 重新设置,就像上面程序演示的那样;

  • 下面这段程序使用 alarm() 设置了一个 5 秒的定时器,每 5 秒会产生一个 SIGALRM 信号:

    #define _POSIX_SOURCE
    ......
    #include <unistd.h>
    ......
    void signal_handler(int sig) {signal(sig, signal_handler);printf("Catch the signal: %d\n",sig);......alarm(5);
    }int main() {......signal(SIGALRM, signal_handler);alarm(5);while (loop) {......}......
    }
    
  • 下面这段代码使用 setitimer() 设置了一个 5 秒的定时器,每 5 秒会产生一个 SIGALRM 信号:

    #define _POSIX_SOURCE
    ......
    #include <sys/time.h>
    ......
    void signal_handler(int sig) {signal(sig, signal_handler);printf("Catch the signal: %d\n",sig);......
    }int main() {......signal(SIGALRM, signal_handler);struct itimerval new_value;new_value.it_value.tv_sec = 5;new_value.it_value.tv_usec = 0;new_value.it_interval.tv_sec = 5;new_value.it_interval.tv_usec = 0;setitimer(ITIMER_REAL, &new_value, NULL);while (loop) {......}......
    }
    
  • 除了编程上的区别外,还要注意 alarm() 需要的头文件是 <unistd.h>,而 setitimer() 需要的头文件是 <sys/time.h>

  • 关于系统调用中的阻塞函数在进程收到信号后会被中止的相关信息可以参考在线手册 man 7 signal,其中 <Interruption of system calls and library functions by signal handlers> 一节中详细介绍了那些阻塞函数可以被信号中止;

  • 另外,阻塞函数被信号中止的功能是 POSIX 标准中的一部分,并不是 libc 默认支持的,所以在程序的开头要加上 #include _POSIX_SOURCE

3 范例

  • 源程序:nonblock-signal.c(点击文件名下载源程序,建议使用UTF-8字符集)演示了使用信号使 socket 编程里的阻塞函数 accept() 每隔 5 秒钟中止一次的过程;

  • 该范例不仅仅是处理了 SIGALRM 信号,还处理了 SIGINT 和 SIGQUIT 信号,旨在说明不仅仅是定时器产生的 SIGALRM 信号会中止阻塞函数,任何信号都会使阻塞函数中止;

  • SIGQUIT 信号可以使用键盘 ctrl + \ 产生,SIGINT 信号就是 ctrl + c

  • 为了程序可以正常退出,程序对 SIGINT 信号做了计数,当按下 ctrl + c 四次时,程序会正常退出;

  • 因为一个 socket 阻塞函数可以被任意信号打断,被打断的函数会返回一个 EINTR 错误,所以在进行 socket 编程时,一定要处理 EINTR;

  • 程序使用 常量 _ALARM_FUNC 控制采用哪种方式设置定时器,当常量 _ALARM_FUNC 已定义时,使用 alarm() 设置定时器,否则使用 setitimer() 设置定时器;

  • 编译:gcc -Wall -g nonblock-signal.c -o nonblock-signal

  • 运行:./nonblock-signal

  • 运行截图:

    GIF of running nonblock-signal

欢迎订阅 『网络编程专栏』


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

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

相关文章

nm命令如何查看目标文件符号表

概述 在Linux环境下&#xff0c;nm 是一个用来查看目标文件&#xff08;如可执行文件、动态库或静态库&#xff09;符号表的工具。使用nm命令可以很方便的查看可执行程序中有哪些函数以及动态库中有哪些导出函数。 nm命令简述 打印结果含义 先使用nm命令查看一个可执行程序…

【IM】如何保证消息可用性(一)

目录 1. 基本概念1.1 长连接 和 短连接1.2 PUSH模式和PULL模式 2. 背景介绍2.1 理解端到端的思想 3. 方案选型3.1 技术挑战3.2 技术目标 1. 基本概念 在讲解消息可用性之前&#xff0c;需要理解几个通信领域的基本概念。 1.1 长连接 和 短连接 什么是长连接&#xff0c;短连接…

MQ面试题之Kafka

前言 前文介绍了消息队列相关知识&#xff0c;并未针对某个具体的产品&#xff0c;所以略显抽象。本人毕业到现在使用的都是公司内部产品&#xff0c;对于通用产品无实际经验&#xff0c;但是各种消息中间件大差不差&#xff0c;故而本次选择一个相对较熟悉的Kafka进行详细介绍…

YoloV8改进策略:BackBone改进|DCNv4最新实践|高效涨点|多种改进教程|完整论文翻译

摘要 涨点效果:在我自己的数据集上,mAP50 由0.986涨到了0.993,mAP50-95由0.737涨到0.77,涨点明显! DCNv4是可变形卷积的第四版,速度和v3相比有了大幅度的提升,但是环境搭建有一定的难度,对新手不太友好。如果在使用过程遇到编译的问题,请严格按照我写的环境配置。 …

编程笔记 html5cssjs 061 JavaScrip简介

编程笔记 html5&css&js 061 JavaScrip简介 一、JavaScript概述二、JavaScript的主要特点三、历史延革四、JavaScript与前端开发小结 JavaScript 是 web 开发者必学的三种语言之一&#xff1a;HTML 定义网页的内容&#xff1b;CSS 规定网页的布局&#xff1b;JavaScript…

RBD —— Fracture SOP

目录 Assemble —— 清理破碎操作并生成碎片 Boolean Fracture —— 使用切割面破碎输入的几何体 Convex Decomposition —— 将输入几何体分解为凸线段 Glue Cluster —— 构建cluster值想glue约束添加强度 RBD Material Fracture —— 基于材质类型预破碎 Concrete Gl…

C++ 之LeetCode刷题记录(二十)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 开始cpp刷题之旅。 依旧是追求耗时0s的一天。 110. 平衡二叉树 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二…

RK3588平台开发系列讲解(视频篇)RKMedia框架

文章目录 一、 RKMedia框架介绍二、 RKMedia框架API三、 视频处理流程四、venc 测试案例沉淀、分享、成长,让自己和他人都能有所收获!😄 📢RKMedia是RK提供的一种多媒体处理方案,可实现音视频捕获、音视频输出、音视频编解码等功能。 一、 RKMedia框架介绍 功能: VI(输…

【Redis笔记】缓存——缓存分类、缓存穿透、缓存雪崩、缓存击穿

缓存 缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于高速存储媒介上。 缓存的本质就是用空间换时间&#xff0c;牺牲数据的实时性&#xff0c;以服务器内存中的数据暂时代替从数据库读取最新的数据&#xff0c;减少数据库IO&#…

2401llvm,匹配器参考

返回类型名字参数Matcher<attr>attrMatcher<Attr>... 匹配属性. 属性可附加各种不同的语法(包括关键字,C11属性,GNU的__attribute和MSVC的__declspec,和#pragma等).也可能是隐含的. 给定 struct [[nodiscard]] Foo{};void bar(int * __attribute__((nonnull)) );…

一起学习ETCD系列——运维操作之etcdctl使用

文章目录 概要一、命令二、实操2.1、基本操作2.2、watch2.3、租约2.4、分布式锁2.5、角色2.6、用户2.7、认证2.8、集群 概要 本文主要用来总结ETCD客户端ctcdctl的命令操作&#xff0c;在运维过程中可能常常用到的。 一、命令 etcd工具 etcdctl官方命令示例 [roottest etcd…

【牛客刷题】笔试选择题整理(day1-day2)

每天都在进步呀 文章目录 1. 小数求模运算2. 进程的分区&#xff0c;这里说的不是JVM的分区。进程中&#xff0c;方法存放在方法区。3. 访问权限控制4. 继承与多态5. 与equals()6. 类加载顺序7. super()与this()7.1 super7.1.1 super调用父类构造方法7.1.2 super调用父类属性和…

私人漫画图书馆:分类管理,一目了然 | 开源日报 No.157

tachiyomiorg/tachiyomi Stars: 26.9k License: Apache-2.0 tachiyomi 是一个免费开源的安卓漫画阅读器。 该项目的主要功能、关键特性、核心优势包括&#xff1a; 从多种来源在线阅读本地阅读已下载内容可配置的阅读器&#xff0c;具有多个查看器、翻页方向和其他设置支持追…

大数据处理系统的架构

大数据处理系统的架构介绍 Lamdba架构 Lambda 架构是一种用于处理大规模数据的设计模式,旨在结合批处理和实时处理,以应对对大量数据进行高效处理的需求。Lambda 架构的核心思想是将数据处理流程分为批处理层和实时处理层,并将它们整合在一起,以获得高可扩展性和灵活性。…

什么叫高斯分布?

高斯分布&#xff0c;也称为正态分布&#xff0c;是统计学中最常见的概率分布之一。它具有钟形曲线的形态&#xff0c;对称分布在均值周围&#xff0c;且由均值和标准差两个参数完全描述。 高斯分布的概率密度函数&#xff08;Probability Density Function, PDF&#xff09;可…

mysql数据库的备份和恢复

登录&#xff1a; mysql -uroot -proot -h127.0.0.1 退出&#xff1a; mysql > exit; mysql > quit; mysql > \q;备份所有数据库 mysqldump命令备份所有数据库的语法如下&#xff1a; mysqldump -u username -p -all-databases > BackupName.sql 示例&#xf…

SpringBoot 有什么优点?

Spring Boot 是一个用于简化和加速 Spring 框架应用程序开发的项目。它构建在 Spring 框架之上&#xff0c;提供了一种快速开发、简化配置和集成的方式。以下是 Spring Boot 的一些优点&#xff1a; 1、简化配置&#xff1a; Spring Boot 使用约定大于配置的理念&#xff0c;通…

[设计模式Java实现附plantuml源码~创建型] 复杂对象的组装与创建——建造者模式

前言&#xff1a; 为什么之前写过Golang 版的设计模式&#xff0c;还在重新写Java 版&#xff1f; 答&#xff1a;因为对于我而言&#xff0c;当然也希望对正在学习的大伙有帮助。Java作为一门纯面向对象的语言&#xff0c;更适合用于学习设计模式。 为什么类图要附上uml 因为很…

DevOps系列文章之 GitLabCI汇总

GitlabCI环境搭建 前提 先安装 docker Docker容器化安装 docker pull gitlab/gitlab-ee:12.4.0-ee.0 创建挂载目录 mkdir -p /srv/gitlab mkdir -p /srv/gitlab/config # 映射到 Glitlab 容器中的配置目录 mkdir -p /srv/gitlab/logs # 映射到 Glitlab 容器中的日志目录 m…

mac裁剪图片

今天第一次用mac裁剪图片&#xff0c;记录一下过程&#xff0c;差点我还以为我要下载photoshop了&#xff0c; 首先准备好图片 裁剪的目的是把图片的标题给去掉&#xff0c;但是不能降低分辨率&#xff0c;否则直接截图就可以了 解决办法 打开原始图片(不要使用预览&#xf…