Linux进程间通信(1)- 信号(signal)机制

本文深入解析Linux中的信号机制,涵盖信号的基本概念、处理方式及应用,包括如何使用signal和kill函数进行信号处理与发送,以及如何利用信号进行进程间的通信。

1 概述
Linux和类Linux系统下进程间通信(Inter-Process Communication, IPC)有很多种方式,包括套接字(socket),共享内存(shared memory),管道(pipe),消息队列(message queue)等[1],各自有各自的一些应用场景和用途,这次就来总结一下通过信号(signal)的机制。

信号,是Linux中向进程发送的消息,接收到该信号的进程会相应地采取一些行动,即通过软中断的方式来响应这个信号,触发一些事先指定或特定的事件。进程之间可以互相通过系统调用kill来发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某件事件[2]。

2 signal的系统api使用
2.1 响应信号

程序可以用signal库函数来处理信号,它的定义如下:

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

这个定义表示,signal是一个带有sig和func两个参数的函数,其中func是一个函数指针,指向的函数带有1个int类型参数且无返回值,signal函数返回值也是一个带有1个int类型参数且无返回值的函数指针。signal函数作用是绑定信号值为sig的信号的响应时间为func指向的函数,即当捕获到sig信号时,调用func指向的函数(可称为信号处理函数),另外func也可以用下面两个特殊值之一来代替信号处理函数:

    SIG_IGN    忽略信号
    SIG_DFL    恢复默认行为

编写一个示例程序C代码:

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

void handler(int sig) {
    printf("this signal no. is %d\n", sig);
    printf("Restore to default.\n");
    signal(sig, SIG_DFL);
}

int main(int argc, char **argv) {
    int sleep_sec = 1;
    int count = 1;
    signal(SIGINT , handler);
    while(1) {
        printf("waiting signal %d secs.\n", count);
        sleep(sleep_sec);
        count += 1;
    }
    return 0;
}

编译运行:


可以发现,当我们按Ctrl+C时,操作系统会给该程序发送信号值为2的信号,其实是SIGINT信号,在main函数中,设定捕获SIGINT信号时,去执行handler函数。正常情况下,程序进行不断的sleep和printf循环,当收到SIGINT信号时,会发生软件中断,保存现场,然后转而去执行信号处理函数,这里也就是handler,执行完后,回到现场,继续执行中断时的后续代码。

在handler信号处理函数里,我们使用了SIG_DFL的值来进行重绑定,于是下一次我们通过键盘按Ctrl+C时,它就中断去执行默认的信号处理函数而不是handler了,于是就退出程序了。

signal函数返回的是先前对指定信号进行处理的信号处理函数的函数指针,如果未定义信号处理函数,则返回SIG_ERR并设置errno为一个正数值,如果给出的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL。

这里需要注意两点:

不同版本的UNIX/LINUX系统,对信号处理方式上有些不同,比如有的是执行完信号处理函数后,自动的将对应的信号的处理函数恢复到默认,而有的不是,所以这里我们希望恢复到默认,最好的方法就是自己写出恢复默认的代码;
信号的值,会因为系统的不同而不同,比如我们这个系统的SIGINT的值是2,可能其他的就不一定
信号的名称是在头文件signal.h中定义的,它们以SIG开头,比如下表:

信号名称说明
SIGABORT *进程异常终止
SIGALRM超时警告
SIGFPE*浮点运算异常
SIGHUP连接挂断
SIGILL *非法指令
SIGINT    终端中断
SIGKILL    终止进程(此信号不能被捕获或忽略)
SIGPIPE向无读进程的管道写数据
SIGQUIT终端退出
SIGSEGV*无效内存段访问
SIGTERM 终止
SIGUSER1 用户定义信号1
SIGUSER2 用户定义信号2

其中带*的信号,系统对该信号的响应视具体实现而定。如果进程接收到上表信号中的任何一个,但事先没有安排捕获它,进程将会立刻终止。通常,系统将会生成core文件,放在当前目录下,该文件是进程在内存中的映像,对程序调试很有用处。当然,系统还有很多其他的信号,比如后文所述,可以通过 kill -l 来查看可发送的信号。

2.2 发送信号
进程可以通过调用kill函数向包括它本身在内的其他进程发送一个信号。如果程序没有权限,kill函数会调用失败,失败的常见原因是目标进程由另一个用户所拥有,这个函数跟shell同名命令kill的功能完全一样,定义如下:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

kill函数中,pid表示要发送信号到达的目标进程的进程id,sig为发送的信号值。

成功时,返回0。失败时,返回-1,并设置 errno变量,errno值包括以下情况:

    errno = EINVAL     给定的信号无效
    errno = EPERM      发送进程权限不够
    errno = ESRCH      目标进程不存在

Linux信号机制向我们提供了一个有用的闹钟功能,进程可以通过调用alarm函数在经过预定时间后发送一个SIGALRM信号:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm函数用来在seconds秒之后安排发送一个SIGALRM信号,但由于处理的延时和时间调度的不确定性,实际闹钟时间比预先安排的要稍微拖后一点儿。如果把参数设置为0,则取消所有已设置的闹钟请求。

还有另外一个有用的信号函数:

#include <unistd.h>

int pause(void);

它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止,才会继续运行它下面的代码。 
这个函数很有用,因为有时我们需要等待某个信号的发生,使用它便意味着程序不需要总是在执行,浪费CPU资源,对系统性能造成极大的影响。

对于信号机制的使用值得一提的是:如果信号出现在系统调用的执行过程中,可能有些系统调用会失败,大多数情况是一些比较慢得调用,比如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。工程实践中使用时,要十分注意和周全考虑。

其实上面介绍的传统UNIX编程中的signal和其相关函数,X/Open和UNIX规范推荐了一个更新更健壮的信号编程接口: sigaction,定义如下:

#include <signal.h>

int sigaction(int sig, const struct sigaction* act, struct sigaction *oact);

3 Linux系统命令使用
想要给一个进程发送信号,而该进程并不是当前的前台进程,就需要使用kill命令[4],它需要一个可选的信号代码,和一个进程ID,例如给PID为520的进程发送挂断信号(SIGHUP),使用如下命令:

    命令格式:kill [参数] [进程号]
    例如:
    kill -HUP 520

如果没有指定信号代码或值,则默认情况下,采用编号为15的TERM信号。TERM信号将终止所有不能捕获该信号的进程。对于那些可以捕获该信号的进程就要用编号为9的kill信号,强行“杀掉”该进程。

    命令参数:
    -l  信号,如果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称
    -a  当处理当前进程时,不限制命令名和进程号的对应关系
    -p  指定kill 命令只打印相关进程的进程号,而不发送任何信号
    -s  指定发送信号
    -u  指定用户

比如可以使用-l参数列出所有支持的信号和他们的值:


kill命令有一个变体,即killall,它可以给运行着某一命令的所有进程发送信号,一般的Linux系统都会有这个命令,如果不知道某进程的pid,或者想给执行相同命名的许多不同的进程发送信号,这条命令就很有用了,一重常用的做法是,通知inetd程序重新读取它的配置选项[1]:

    killall -HUP inetd

参考文献
[1] Linux程序设计(第4版)

[2] Linux信号(signal)机制分析, http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html

[3] 每天一个linux命令(42):kill命令,http://www.cnblogs.com/peida/archive/2012/12/20/2825837.html

<think> 我们被要求详细讲解Linux进程间通信信号量。根据用户提供的引用,我们需要整合这些信息,并构建一个详细的回答。 引用[1]提到有名信号量(Named Semaphore)是由操作系统内核维护的,具有全局唯一名字,可以在不同进程之间共享。 引用[2]介绍了信号是软中断,用于进程间通信和处理异常,是异步通知机制。 引用[3]解释了信号量是一种计数器,用于协调多个进程对共享资源的访问。它描述了信号量的基本操作过程:当进程想要使用共享资源时,先测试信号量,如果值大于0则使用并减1,否则挂起等待;当资源被释放时,信号量加1。 引用[4]提到了同步的概念,即进程之间按照依赖关系运行,并提到实现信号量的操作。 注意:这里用户询问的是“信号量”(semaphore),而引用[2]中提到了“信号”(signal),这是两个不同的概念。我们需要区分清楚,避免混淆。 根据以上引用,我们组织回答如下: 1. 信号量的定义和作用:信号量是一个计数器,用于控制多个进程对共享资源的访问。它主要用于进程间的同步和互斥。 2. 信号量的类型:分为有名信号量和无名信号量(也叫基于内存的信号量)。有名信号量通过名字在系统中唯一标识,可用于不同进程之间;无名信号量通常存在于内存中,一般用于同一进程内的线程间同步,但也可以通过共享内存用于进程间。 3. 信号量的操作:P操作(等待,信号量减1)和V操作(释放,信号量加1)。当信号量值为0时,试图进行P操作的进程将被阻塞,直到有其他进程执行V操作。 4. 在Linux中,信号量的实现:System V信号量和POSIX信号量。POSIX信号量又分为有名信号量和无名信号量。 5. 使用示例:我们将给出一个简单的使用POSIX有名信号量进行进程间同步的代码示例。 注意:由于引用中提到了有名信号量,我们将重点放在有名信号量的介绍上,但也会简要提及无名信号量。 我们将按照以下结构组织回答: 1. 信号量的基本概念 2. 信号量的类型(有名信号量和无名信号量) 3. 信号量的工作原理(P/V操作) 4. 信号量的操作函数(POSIX有名信号量) 5. 使用示例 6. 注意事项 同时,根据要求,我们需要在最后生成相关问题。 注意:引用标识要自然地添加在引用内容的末尾。我们参考了用户提供的引用[1][3][4](引用[2]是关于信号的,与信号量不同,所以我们不引用它)。 下面开始回答: </think> ### Linux 进程间通信信号量详解 信号量(Semaphore)是 Linux 进程间通信(IPC)的核心机制之一,本质是一个**受保护的计数器**,用于协调多个进程对共享资源的访问控制。其核心功能是通过原子操作确保资源的互斥访问和同步协调。 --- #### **1. 信号量基础原理** - **计数器机制**: 信号量维护一个整数值,表示可用资源的数量: - 值 $ > 0 $:当前有可用资源 - 值 $ = 0 $:资源耗尽,请求进程需等待 - 值 $ < 0 $:绝对值表示等待队列中的进程数量 - **P/V 操作**(原语操作): - **P 操作**(`sem_wait()`): $$ P(S): \text{ if } S > 0 \text{ then } S \leftarrow S-1 \text{ else block} $$ 进程尝试获取资源,若信号量值 $ S \leq 0 $ 则阻塞 - **V 操作**(`sem_post()`): $$ V(S): S \leftarrow S+1 \text{, wake a blocked process} $$ 进程释放资源,唤醒等待队列中的进程[^3] --- #### **2. 信号量类型** | 类型 | 标识方式 | 作用范围 | 生命周期 | |------|----------|----------|----------| | **有名信号量** | 文件系统路径名 (如 `/mysem`) | 跨进程共享 | 持续到显式删除或系统重启 | | **无名信号量** | 共享内存地址 | 父子进程/线程间 | 随进程终止销毁[^1] | > 有名信号量通过全局唯一路径名访问,适合无亲缘关系进程;无名信号量需配合共享内存使用,适合相关进程。 --- #### **3. POSIX 信号量核心 API** ```c #include <semaphore.h> // 创建/打开有名信号量 sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); // 初始化无名信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); // P操作(等待资源) int sem_wait(sem_t *sem); // 阻塞等待 int sem_trywait(sem_t *sem); // 非阻塞尝试 // V操作(释放资源) int sem_post(sem_t *sem); // 关闭信号量 int sem_close(sem_t *sem); // 删除有名信号量 int sem_unlink(const char *name); ``` **参数说明**: - `value`:信号量初始值(资源数量) - `pshared`:`0`为线程共享,`非0`为进程共享 - `oflag`:`O_CREAT`(不存在则创建)或 `O_EXCL`(配合`O_CREAT`确保新建) --- #### **4. 使用场景与同步模型** 1. **互斥锁(Mutex)**: 初始值设为1,实现临界区保护: ```c sem_t mutex; sem_init(&mutex, 1, 1); // 进程间共享,初始值1 // 进程A sem_wait(&mutex); // 进入临界区 /* 访问共享资源 */ sem_post(&mutex); // 离开临界区 ``` 2. **生产者-消费者问题**: 使用两个信号量控制缓冲区的空/满状态: ```c sem_t empty, full; sem_init(&empty, 1, BUFFER_SIZE); // 初始空槽数量 sem_init(&full, 1, 0); // 初始满槽数量 // 生产者 sem_wait(&empty); // 等待空槽 /* 生产数据放入缓冲区 */ sem_post(&full); // 增加满槽计数 // 消费者 sem_wait(&full); // 等待满槽 /* 取出缓冲区数据 */ sem_post(&empty); // 增加空槽计数 ``` --- #### **5. 实战示例:有名信号量同步进程** ```c #include <fcntl.h> #include <semaphore.h> #include <stdio.h> #include <unistd.h> int main() { sem_t *sem = sem_open("/demo_sem", O_CREAT, 0644, 1); // 初始值=1 if (fork() == 0) { // 子进程 sem_wait(sem); printf("Child enters critical section\n"); sleep(2); // 模拟操作共享资源 printf("Child exits critical section\n"); sem_post(sem); } else { // 父进程 sem_wait(sem); printf("Parent enters critical section\n"); sleep(1); // 模拟操作共享资源 printf("Parent exits critical section\n"); sem_post(sem); wait(NULL); sem_unlink("/demo_sem"); // 清理信号量 } return 0; } ``` **输出**: ```plaintext Parent enters critical section Parent exits critical section Child enters critical section # 子进程等待父进程释放信号量 Child exits critical section ``` --- #### **6. 信号量与信号的区别** | 特性 | 信号(Semaphore) | 信号 (Signal) | |--------------|----------------------------------|-----------------------------| | **本质** | 资源计数器 | 软件中断通知 | | **用途** | 控制共享资源访问 | 事件通知/异常处理 | | **通信方向** | 无明确发送方/接收方 | 有明确发送方(如`kill()`) | | **同步机制** | 内置阻塞/唤醒队列 | 依赖信号处理函数 | | **数据携带** | 仅计数器数值 | 可附带`siginfo_t`信息[^2] | --- #### **7. 注意事项** 1. **死锁预防**:避免循环等待(如进程A持有信号量S1等待S2,进程B持有S2等待S1) 2. **信号量泄漏**:确保`sem_close()`/`sem_unlink()`配对调用 3. **原子性保障**:P/V 操作是原子指令,确保计数器更新不可分割 4. **优先级反转**:高优先级进程可能因等待低优先级进程持有的信号量而阻塞 > 信号量是实现进程同步的底层原语,在数据库连接池、线程池等资源受限场景中广泛应用[^4]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值