Linux应用进程间通信六(信号)

Linux应用进程间通信六(信号)

一、概述

1.1、信号的概念

  信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。通常信号是由一个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程。一个信号的产生叫生成,接收到一个信号叫捕获。

  1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

  2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

  3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被 取消时才被传递给进程。

1.2、用户进程对信号的响应方式

  1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。

  2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。

  3)执行缺省操作:Linux对每种信号都规定了默认操作

1.3、信号的来源

在Linux操作系统中,信号可以由多种来源产生,包括但不限于:

  1. 用户操作:用户可以通过键盘产生信号,如按下Ctrl+C通常发送SIGINT(中断信号)。
  2. 软件生成:程序可以通过kill系统调用或raise函数发送信号给其他进程或自身。
  3. 硬件异常:当程序执行非法操作(如除零、内存访问违规)时,硬件会触发信号,如SIGFPE(浮点异常)或SIGSEGV(段错误)。
  4. 系统条件:系统在特定条件下也会发送信号,如SIGHUP(挂起信号)可能在控制终端关闭时发送。
  5. 定时器超时:使用alarm、setitimer或timer等定时器函数设置的定时器超时后,会发送如SIGALRM或SIGVTALRM信号。

1.4、信号的种类

Linux中定义了许多种类的信号,每种信号都有其特定的用途和默认行为。以下是一些常见的信号:

  1. SIGINT:中断信号,通常由用户通过按下Ctrl+C产生,用于中断正在运行的程序。
  2. SIGTERM:终止信号,用于请求程序自己终止,可以被捕获或忽略。
  3. SIGKILL:杀死信号,用于立即终止程序,无法被捕获或忽略。
  4. SIGSTOP:停止信号,用于停止进程的执行,无法被捕获、忽略或由用户生成。
  5. SIGCHLD:子进程结束信号,当子进程结束时发送给父进程。
  6. SIGCONT:继续信号,用于唤醒一个被停止的进程。

此外,还有SIGSEGV(段错误信号)、SIGFPE(浮点异常信号)、SIGILL(非法指令信号)等,分别用于处理不同的异常和事件。  

二、实现相关函数

2.1、信号的发送函数

  1. kill函数

    • 原型int kill(pid_t pid, int sig);
    • 功能:向指定进程发送信号。其中,pid指定目标进程的ID,sig指定要发送的信号。
    • 参数
      • pid:目标进程的ID。取值有以下情况:
        • 大于0:将信号发送给指定进程ID的进程。
        • 等于0:将信号发送给当前进程组中的所有进程。
        • 小于-1:将信号发送给进程组号为pid绝对值的进程组中的所有进程。
      • sig:要发送的信号编号,推荐使用信号宏定义。
    • 返回值:成功返回0,失败返回-1。
  2. raise函数

    • 原型int raise(int sig);
    • 功能:向当前进程发送信号。相当于kill(getpid(), sig);
    • 参数sig为要发送的信号编号。
    • 返回值:成功返回0,失败返回非0值。
  3. abort函数

    • 原型void abort(void);
    • 功能:向当前进程发送异常终止信号SIGABRT,并产生core文件。相当于kill(getpid(), SIGABRT);
  4. alarm函数

    • 原型unsigned int alarm(unsigned int seconds);
    • 功能:设置定时器,指定seconds秒后,内核给当前进程发送SIGALRM信号。进程收到该信号,默认终止进程。
    • 参数seconds为定时器超时时间,单位为秒。
    • 返回值:返回上次alarm设置的剩余秒数。若设置为0,则表示取消定时器,并返回旧闹钟剩余秒数。
  5. setitimer函数

    • 原型int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
    • 功能:设置定时器,可替代alarm函数,精度更高(微秒级),可实现周期定时。
    • 参数
      • which:定时方式,有以下选项:
        • ITIMER_REAL:计算自然时间,发送SIGALRM信号。
        • ITIMER_VIRTUAL:只计算用户空间占用的CPU时间,发送SIGVTALRM信号。
        • ITIMER_PROF:计算用户空间和内核空间占用的CPU时间之和,发送SIGPROF信号。
      • new_value:指向itimerval结构的指针,指定定时器的超时时间和周期。
      • old_value:指向itimerval结构的指针,用于存储旧的定时器值,通常设为NULL。

2.2、信号的接收与等待函数

  • pause函数

    • 原型int pause(void);
    • 功能:让调用者进入休眠状态,直到进程收到信号并被唤醒。
    • 返回值:唤醒后返回-1。
  • sleep函数

    • 原型unsigned int sleep(unsigned int seconds);
    • 功能:让调用者进入休眠状态指定的秒数,期间可被信号唤醒。
    • 返回值:剩余的休眠时间。

2.3、信号的处理函数

  1. signal函数

    • 原型sighandler_t signal(int signum, sighandler_t handler);
    • 功能:向内核注册一个信号处理函数。
    • 参数
      • signum:要处理的信号编号。
      • handler:自定义的信号处理函数的指针。
    • 返回值:设置成功返回以前的信号处理程序的地址,设置失败返回SIG_ERR。
  2. sigaction函数

    • 原型int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    • 功能:设置信号处理函数,比signal函数更灵活,可用于实时信号。
    • 参数
      • signum:要处理的信号编号。
      • act:指向sigaction结构的指针,指定新的信号处理函数和其他选项。
      • oldact:指向sigaction结构的指针,用于存储旧的信号处理函数信息,通常设为NULL。

2.4、信号的屏蔽与解除屏蔽函数

  • sigprocmask函数

    • 原型int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    • 功能:设置要屏蔽的信号。
    • 参数
      • how:指定屏蔽信号的方式,有以下选项:
        • SIG_BLOCK:添加set中的信号到当前屏蔽集。
        • SIG_UNBLOCK:从当前屏蔽集中删除set中的信号。
        • SIG_SETMASK:用set替换当前的屏蔽集。
      • set:指向sigset_t结构的指针,指定要屏蔽或解除屏蔽的信号集。
      • oldset:指向sigset_t结构的指针,用于存储旧的屏蔽集信息,通常设为NULL。

三、实例应用

3.1、kill 函数发送杀死pid指向的进程的信号,raise 发送杀死自己的信号  

来看如何使用kill函数: 

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    pid_t pid = atoi(argv[1]);
    if (kill(pid, SIGKILL) == -1) {
        perror("kill");
        exit(EXIT_FAILURE);
    }

    printf("Sent SIGKILL to process %d\n", pid);
    return 0;
}

在这个例子中,程序接受一个命令行参数,即要终止的进程的PID。然后,它使用kill函数向该进程发送SIGKILL信号。如果kill调用失败,程序将打印错误信息并退出。

接下来,看如何使用raise函数:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int main() {
    printf("Sending SIGKILL to myself...\n");
    if (raise(SIGKILL) == -1) {
        perror("raise");
        // Note: If raise fails, it's highly unlikely we'll get here,
        // because SIGKILL is uncatchable and usually terminates the process immediately.
        // But for the sake of completeness, we include this check.
        exit(EXIT_FAILURE);
    }

    // This line will never be reached because the process will be terminated by SIGKILL.
    printf("This will not be printed.\n");

    return 0; // This return statement will never be executed.
}

 在这个例子中,程序直接调用raise函数向自己发送SIGKILL信号。由于SIGKILL信号是无法被捕获或忽略的,因此程序将立即终止。因此,raise调用之后的任何代码都不会被执行。

注意:在实际使用中,发送SIGKILL信号应该谨慎进行,因为它会强制终止进程,不给进程任何清理资源或保存状态的机会。通常,应该首先尝试发送SIGTERM信号,让进程有机会进行清理工作,然后再考虑使用SIGKILL

另外,编译和运行这些程序需要适当的权限。特别是,终止其他用户的进程通常需要超级用户权限。在编译这些程序时,可以使用gcc编译器,例如:

gcc -o kill_example kill_example.c
gcc -o raise_example raise_example.c

然后,以适当的权限运行它们:

./kill_example <pid> # 替换<pid>为目标进程的PID
./raise_example # 注意,这将立即终止当前运行的程序

3.2、定时器的使用

在Linux应用进程间通信中,信号定时器(timer)是一种常用的机制,用于在指定的时间间隔后或在指定的时间点向进程发送信号。这通常通过alarm函数或更灵活的setitimer函数来实现。下面是一个使用setitimer函数设置定时器的简单示例。 

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
 
// 信号处理函数
void timer_handler(int signum) {
    static int count = 0;
    printf("timer expired %d times\n", ++count);
}
 
int main() {
    // 设置信号处理函数
    struct sigaction sa;
    sa.sa_handler = &timer_handler; // 指定处理函数
    sa.sa_flags = 0; // 默认标志
    sigemptyset(&sa.sa_mask); // 清空信号集
    if (sigaction(SIGVTALRM, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }
 
    // 设置定时器
    struct itimerval timer;
    timer.it_value.tv_sec = 2; // 初始延迟2秒
    timer.it_value.tv_usec = 0; // 微秒部分
    timer.it_interval.tv_sec = 1; // 每隔1秒触发一次
    timer.it_interval.tv_usec = 0; // 微秒部分
 
    if (setitimer(ITIMER_VIRTUAL, &timer, NULL) == -1) {
        perror("setitimer");
        exit(EXIT_FAILURE);
    }
 
    // 主循环,等待信号
    while (1) {
        pause(); // 等待信号
    }
 
    // 注意:由于使用了pause(),这个return语句实际上永远不会被执行。
    // 进程将在收到SIGVTALRM信号时被唤醒,并调用timer_handler函数。
    return 0;
}

在这个例子中,设置了一个虚拟定时器(ITIMER_VIRTUAL),它将在初始延迟2秒后首次触发,并且之后每隔1秒触发一次。定时器触发时,将向进程发送SIGVTALRM信号,该信号由我们自定义的timer_handler函数处理。

timer_handler函数简单地打印出一个计数器,每次定时器触发时递增。

main函数中的while (1)循环和pause()调用确保进程在等待信号时不会退出。pause()函数会使进程进入休眠状态,直到它接收到一个信号。一旦收到信号,pause()将返回,进程将继续执行(在这个例子中,实际上会再次进入pause()调用,等待下一个信号)。

请注意,由于使用了pause()和无限循环,这个程序将一直运行,直到被外部方式终止(例如,通过发送SIGKILL信号)。

编译和运行这个程序:

gcc -o timer_example timer_example.c
./timer_example

应该会看到每隔一秒打印一次的计数器输出,表明定时器正在按预期工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值