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

相关文章

C++变换迭代器使用方法小结

《C++变换迭代器使用方法小结》本文主要介绍了C++变换迭代器使用方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、源码2、代码解析代码解析:transform_iterator1. transform_iterat

C++中std::distance使用方法示例

《C++中std::distance使用方法示例》std::distance是C++标准库中的一个函数,用于计算两个迭代器之间的距离,本文主要介绍了C++中std::distance使用方法示例,具... 目录语法使用方式解释示例输出:其他说明:总结std::distance&n编程bsp;是 C++ 标准

vue使用docxtemplater导出word

《vue使用docxtemplater导出word》docxtemplater是一种邮件合并工具,以编程方式使用并处理条件、循环,并且可以扩展以插入任何内容,下面我们来看看如何使用docxtempl... 目录docxtemplatervue使用docxtemplater导出word安装常用语法 封装导出方

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

揭秘Python Socket网络编程的7种硬核用法

《揭秘PythonSocket网络编程的7种硬核用法》Socket不仅能做聊天室,还能干一大堆硬核操作,这篇文章就带大家看看Python网络编程的7种超实用玩法,感兴趣的小伙伴可以跟随小编一起... 目录1.端口扫描器:探测开放端口2.简易 HTTP 服务器:10 秒搭个网页3.局域网游戏:多人联机对战4.

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

使用C#代码在PDF文档中添加、删除和替换图片

《使用C#代码在PDF文档中添加、删除和替换图片》在当今数字化文档处理场景中,动态操作PDF文档中的图像已成为企业级应用开发的核心需求之一,本文将介绍如何在.NET平台使用C#代码在PDF文档中添加、... 目录引言用C#添加图片到PDF文档用C#删除PDF文档中的图片用C#替换PDF文档中的图片引言在当

Kotlin 作用域函数apply、let、run、with、also使用指南

《Kotlin作用域函数apply、let、run、with、also使用指南》在Kotlin开发中,作用域函数(ScopeFunctions)是一组能让代码更简洁、更函数式的高阶函数,本文将... 目录一、引言:为什么需要作用域函数?二、作用域函China编程数详解1. apply:对象配置的 “流式构建器”最