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 来获取该信息并释放资源,
    // 那么僵尸进程将一直存在,直到父进程终止。这会导致资源泄漏问题
}
相关文章
|
11月前
|
消息中间件 存储 网络协议
从零开始掌握进程间通信:管道、信号、消息队列、共享内存大揭秘
本文详细介绍了进程间通信(IPC)的六种主要方式:管道、信号、消息队列、共享内存、信号量和套接字。每种方式都有其特点和适用场景,如管道适用于父子进程间的通信,消息队列能传递结构化数据,共享内存提供高速数据交换,信号量用于同步控制,套接字支持跨网络通信。通过对比和分析,帮助读者理解并选择合适的IPC机制,以提高系统性能和可靠性。
1515 14
|
存储 算法 Linux
C语言 多进程编程(一)进程创建
本文详细介绍了Linux系统中的进程管理。首先,文章解释了进程的概念及其特点,强调了进程作为操作系统中独立可调度实体的重要性。文章还深入讲解了Linux下的进程管理,包括如何获取进程ID、进程地址空间、虚拟地址与物理地址的区别,以及进程状态管理和优先级设置等内容。此外,还介绍了常用进程管理命令如`ps`、`top`、`pstree`和`kill`的使用方法。最后,文章讨论了进程的创建、退出和等待机制,并展示了如何通过`fork()`、`exec`家族函数以及`wait()`和`waitpid()`函数来管理和控制进程。此外,还介绍了守护进程的创建方法。
C语言 多进程编程(一)进程创建
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
3月前
|
存储 C语言
`scanf`是C语言中用于按格式读取标准输入的函数
`scanf`是C语言中用于按格式读取标准输入的函数,通过格式字符串解析输入并存入指定变量。需注意输入格式严格匹配,并建议检查返回值以确保读取成功,提升程序健壮性。
1060 0
|
11月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
722 23
|
5月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
341 15