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

2024-01-28 10:12

本文主要是介绍使用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

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


这篇关于使用signal中止阻塞的socket函数的应用实例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/653322

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4

Redis 基本数据类型和使用详解

《Redis基本数据类型和使用详解》String是Redis最基本的数据类型,一个键对应一个值,它的功能十分强大,可以存储字符串、整数、浮点数等多种数据格式,本文给大家介绍Redis基本数据类型和... 目录一、Redis 入门介绍二、Redis 的五大基本数据类型2.1 String 类型2.2 Hash

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三