使用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

相关文章

SpringBoot中使用Flux实现流式返回的方法小结

《SpringBoot中使用Flux实现流式返回的方法小结》文章介绍流式返回(StreamingResponse)在SpringBoot中通过Flux实现,优势包括提升用户体验、降低内存消耗、支持长连... 目录背景流式返回的核心概念与优势1. 提升用户体验2. 降低内存消耗3. 支持长连接与实时通信在Sp

python使用库爬取m3u8文件的示例

《python使用库爬取m3u8文件的示例》本文主要介绍了python使用库爬取m3u8文件的示例,可以使用requests、m3u8、ffmpeg等库,实现获取、解析、下载视频片段并合并等步骤,具有... 目录一、准备工作二、获取m3u8文件内容三、解析m3u8文件四、下载视频片段五、合并视频片段六、错误

CSS中的Static、Relative、Absolute、Fixed、Sticky的应用与详细对比

《CSS中的Static、Relative、Absolute、Fixed、Sticky的应用与详细对比》CSS中的position属性用于控制元素的定位方式,不同的定位方式会影响元素在页面中的布... css 中的 position 属性用于控制元素的定位方式,不同的定位方式会影响元素在页面中的布局和层叠关

gitlab安装及邮箱配置和常用使用方式

《gitlab安装及邮箱配置和常用使用方式》:本文主要介绍gitlab安装及邮箱配置和常用使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1.安装GitLab2.配置GitLab邮件服务3.GitLab的账号注册邮箱验证及其分组4.gitlab分支和标签的

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

nginx启动命令和默认配置文件的使用

《nginx启动命令和默认配置文件的使用》:本文主要介绍nginx启动命令和默认配置文件的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录常见命令nginx.conf配置文件location匹配规则图片服务器总结常见命令# 默认配置文件启动./nginx

在Windows上使用qemu安装ubuntu24.04服务器的详细指南

《在Windows上使用qemu安装ubuntu24.04服务器的详细指南》本文介绍了在Windows上使用QEMU安装Ubuntu24.04的全流程:安装QEMU、准备ISO镜像、创建虚拟磁盘、配置... 目录1. 安装QEMU环境2. 准备Ubuntu 24.04镜像3. 启动QEMU安装Ubuntu4

使用Python和OpenCV库实现实时颜色识别系统

《使用Python和OpenCV库实现实时颜色识别系统》:本文主要介绍使用Python和OpenCV库实现的实时颜色识别系统,这个系统能够通过摄像头捕捉视频流,并在视频中指定区域内识别主要颜色(红... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间详解

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

Python常用命令提示符使用方法详解

《Python常用命令提示符使用方法详解》在学习python的过程中,我们需要用到命令提示符(CMD)进行环境的配置,:本文主要介绍Python常用命令提示符使用方法的相关资料,文中通过代码介绍的... 目录一、python环境基础命令【Windows】1、检查Python是否安装2、 查看Python的安