C语言 多进程编程(四)定时器信号和子进程退出信号

简介: 本文详细介绍了Linux系统中的定时器信号及其相关函数。首先,文章解释了`SIGALRM`信号的作用及应用场景,包括计时器、超时重试和定时任务等。接着介绍了`alarm()`函数,展示了如何设置定时器以及其局限性。随后探讨了`setitimer()`函数,比较了它与`alarm()`的不同之处,包括定时器类型、精度和支持的定时器数量等方面。最后,文章讲解了子进程退出时如何利用`SIGCHLD`信号,提供了示例代码展示如何处理子进程退出信号,避免僵尸进程问题。

多进程(四)

定时器信号

SIGALRM 信号是用来通知进程的定时器到期的。它是一个非可靠信号,即使进程捕获了它也不能保证定时器一定会到期。

定时器信号的使用场景:

  1. 定时器信号可以用来实现计时器功能。
  2. 定时器信号可以用来实现超时重试功能。
  3. 定时器信号可以用来实现定时任务功能。

img_39.png

alarm()函数

alarm()函数用来设置一个定时器,单位是秒。当定时器到期时,会向进程发送SIGALRM信号。
函数原型:

unsigned int alarm(unsigned int seconds);

返回值:

  • 如果成功设置定时器,则返回之前的定时器值(以秒为单位)。
  • 如果定时器未设置,则返回0。

参数说明:

  • seconds:定时器的秒数。

要点:

定时器的定时任务由内核完成, alarm 函数值负责设置定时时间, 并告诉内核启动定时器
当定时时间超时后,内核会向进程发出 SIGALRM 信号

示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

//信号处理函数
void do_sig_alarm(int sig);
int main(){
   
   
    unsigned int ret;

    ret= alarm(5);//设置定时器,5秒,由内核来进行定时,时间结束后会发送SIGALRM信号
    printf("定时器返回: %d\n", ret);//ret为0,前一次设置定时器或者定时器时间已经用完

//    ret= alarm(5);//设置定时器,5秒,由内核来进行定时
//    printf("定时器返回: %d\n", ret);//ret为5

    __sighandler_t r;//信号处理函数的返回值
    r=signal(SIGALRM,do_sig_alarm);
    if(r==SIG_ERR){
   
   //出错处理
        perror("signal");//出错处理
        exit(1);//退出程序
    }
    pause();//暂停程序,等待定时器超时,时间结束后会发送SIGALRM信号 默认是结束进程


    return 0;
}

//信号处理函数
void do_sig_alarm(int sig){
   
   
    printf("收到定时器超时信号 : %d\n", sig);
}

alarm()函数的限制

  • 定时器信号是非可靠信号,即使进程捕获了它也不能保证定时器一定会到期。
  • 定时器信号只对进程有效,对子进程不起作用。
  • 定时器信号只对定时器设置的时间有效,不会影响到其他的定时器。

定时器信号的实现原理

  • 定时器信号是由内核来进行定时,并向进程发送SIGALRM信号。
  • 定时器信号的实现依赖于系统调用alarm()函数。
  • 定时器信号的实现原理是,内核在进程的进程控制块(PCB)中设置一个定时器,并将定时器的到期时间写入到进程的用户态的定时器中。
  • 当定时器到期时,内核向进程发送SIGALRM信号,进程捕获到信号后,可以执行定时器到期的任务。

setitimer()函数

setitimer()函数用来设置一个定时器,单位是秒。当定时器到期时,会向进程发送SIGALRM信号。


setitimer()和alarm()函数的区别

setitimer() 和 alarm() 函数在操作系统中都用于设置定时器,但它们有一些显著的区别:

setitimer()

setitimer() 函数更为灵活和精细,它可以设置三种类型的定时器,分别是:

ITIMER_REAL:实际时间定时器,当定时器到期时,会发送 SIGALRM 信号。

ITIMER_VIRTUAL:与进程在用户态下执行时间相关,当进程在用户态下运行时间到期时,会发送 SIGVTALRM 信号。

ITIMER_PROF:与进程在用户态和内核态下执行时间相关,当定时器到期时,会分别发送 SIGPROF 信号。

它的函数原型如下:

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数解释:
which:选择哪种定时器(ITIMER_REAL、ITIMER_VIRTUAL 或 ITIMER_PROF)。
new_value:指向一个 itimerval 结构,该结构指定定时器的初始响应时间和周期性时间间隔。
old_value:指向一个 itimerval 结构,如果不是 NULL,将返回先前的定时器设置。

old_value是可选参数,如果不为 NULL,则函数调用成功后,old_value 结构将包含先前的定时器设置。

itimerval结构体定义如下:
struct itimerval {
   
   
     struct timeval it_interval; /* 间隔时间 */
     struct timeval it_value;    /* 到期时间 */
 };

 timeval结构体定义如下:
 struct timeval {
   
   
     time_t tv_sec;    /* 秒 */
     suseconds_t tv_usec; /* 微秒 */
 };

示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/time.h>

void do_sig_alarm(int sig);

int main(){
   
   
    struct itimerval itv;//定义定时器结构体
    itv.it_value.tv_sec=3;//设置定时器时间为5秒
    itv.it_value.tv_usec=0;//设置微秒为0
    itv.it_interval.tv_sec=5;//设置定时器间隔时间为5秒
    itv.it_interval.tv_usec=0;//设置微秒为0


    if(setitimer(ITIMER_REAL,&itv,NULL)!=0){
   
   //设置定时器
        perror("setitimer");
        exit(1);
    }
    __sighandler_t r;//信号处理函数的返回值
    r=signal(SIGALRM,do_sig_alarm);
    if(r==SIG_ERR){
   
   //出错处理
        perror("signal");//出错处理
        exit(1);//退出程序
    }
    while(1) {
   
   
        pause();//暂停程序等待信号
    }
    return 0;
}

void do_sig_alarm(int sig){
   
   
    printf("收到定时器超时信号 : %d\n", sig);
}

old_value参数的示例

old_value参数用于返回setitimer()函数调用之前的定时器设置。这对于获取定时器的先前状态或恢复定时器的原始状态非常有用

示例程序,演示如何使用old_value参数来保存和恢复定时器的原始状态:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

// 定时器处理函数
void timer_handler(int signum) {
   
   
    printf("Timer expired\n");
}

int main() {
   
   
    struct itimerval timer, old_timer;

    // 设置信号处理程序
    signal(SIGALRM, timer_handler);

    // 初始化新定时器设置
    timer.it_value.tv_sec = 5;        // 定时器第一次触发的时间(秒)
    timer.it_value.tv_usec = 0;       // 定时器第一次触发的时间(微秒)
    timer.it_interval.tv_sec = 1;     // 定时器周期性触发的时间间隔(秒)
    timer.it_interval.tv_usec = 0;    // 定时器周期性触发的时间间隔(微秒)

    // 设置新的定时器,并获取旧的定时器设置
    if (setitimer(ITIMER_REAL, &timer, &old_timer) == -1) {
   
   
        perror("setitimer");
        return 1;
    }

    // 打印旧的定时器设置
    printf("Old timer:\n");
    printf("it_value: %ld sec, %ld usec\n", (long)old_timer.it_value.tv_sec, (long)old_timer.it_value.tv_usec);
    printf("it_interval: %ld sec, %ld usec\n", (long)old_timer.it_interval.tv_sec, (long)old_timer.it_interval.tv_usec);

    // 等待定时器触发
    while (1) {
   
   
        pause(); // 让进程一直等待信号打断
    }

    return 0;
}

运行结果:

程序运行时会输出旧定时器的设置,并在5秒后第一次触发定时器,然后每秒触发一次,输出"Timer expired"。

通过这种方式,你可以使用old_value参数来保存和恢复以前的定时器设置。

对比alarm()

alarm() 函数是一个简单的工具,用于设置一个秒级别的定时器,
当这个定时器到期时,操作系统会发送一个 SIGALRM 信号给调用进程。
它只有一种模式,而且只能设置一个定时器。

它的函数原型如下:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

seconds:指定定时器的时间(秒)。

区别总结:

定时器类型:

setitimer() 支持三种定时器类型(实际时间、用户态时间、用户态和内核态时间),更灵活。
alarm() 只支持实际时间定时器。

精度:

setitimer() 支持微秒级精度。

alarm() 只支持秒级精度。

支持多个定时器:

setitimer() 可以同时设置和管理多个不同类型的定时器。

alarm() 只能设置一个定时器,后续调用会覆盖之前的定时器。

子进程退出信号

在使⽤ wait() 函数时,由于阻塞或者⾮阻塞都⾮常消耗资源,并且在阻塞情况下,⽗
进程不能执⾏其他逻辑

⼦进程退出是异步事件, 可以利⽤在⼦进程退出时,会⾃动给⽗进程发送 SIGCHLD 信号
img_40.png

示例:

void do_sig_child(int sig);

int main(){
   
   
    pid_t cpid;//子进程id


    __sighandler_t r;//信号处理函数的返回值
    r=signal(SIGCHLD,do_sig_child);
    if(r==SIG_ERR){
   
   //出错处理
        perror("signal");//出错处理
        exit(1);//退出程序
    }


    cpid=fork();//创建子进程
    if(cpid==-1){
   
   //出错处理
        perror("fork");//出错处理
        exit(1);//退出程序
    }else if(cpid==0){
   
   //子进程
        printf("子进程pid: %d\n", getpid());
        sleep(5);//睡眠5秒
        exit(999);//退出子进程
    }else{
   
   //父进程
        printf("父进程pid: %d\n", getpid());
        //父进程不会因为等待子进程而阻塞,会继续往下执行
        while (1){
   
   

        }
        printf("父进程结束\n");
    }

    return 0;

}
//子进程退出后会发送SIGCHLD, SIGCHLD默认处理方式是忽略的,内核现在会调用这个函数
void do_sig_child(int sig){
   
   
    printf("SIGCHLD处理函数: %s\n", strsignal(sig));//打印 : SIGCHLD处理函数: Child exited
    wait(NULL  );//不会阻塞会立即释放子进程资源
    //通常情况下,当一个子进程终止时,它会进入一种称为“僵尸进程”的状态,
    // 保留一些必要的信息以便父进程能够获取其终止状态。
    // 如果父进程不调用 wait 或 waitpid 来获取该信息并释放资源,
    // 那么僵尸进程将一直存在,直到父进程终止。这会导致资源泄漏问题
}
相关文章
|
2月前
|
安全 开发者 Python
揭秘Python IPC:进程间的秘密对话,让你的系统编程更上一层楼
【9月更文挑战第8天】在系统编程中,进程间通信(IPC)是实现多进程协作的关键技术。IPC机制如管道、队列、共享内存和套接字,使进程能在独立内存空间中共享信息,提升系统并发性和灵活性。Python提供了丰富的IPC工具,如`multiprocessing.Pipe()`和`multiprocessing.Queue()`,简化了进程间通信的实现。本文将从理论到实践,详细介绍各种IPC机制的特点和应用场景,帮助开发者构建高效、可靠的多进程应用。掌握Python IPC,让系统编程更加得心应手。
30 4
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
2月前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
2月前
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
17天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
30 3
|
8天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
27 10
|
1天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
7天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
32 7
|
7天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
21 4
|
12天前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。

相关实验场景

更多