linux IPC之信号

信号可以说是ipc最复杂的的通信方式。在网上也有很多很多讲解得比较好的资料可以参考,我在这里只是初略的对自己的学习的内容做个总结!

信号本质

信号实际上是软中断,既然是中断那么信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号来源

信号事件的发生有两个来源:硬件来源即硬件中断(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

信号分类

  1. 不可靠信号:Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的信号叫做”不可靠信号”,信号值小于SIGRTMIN的信号都是不可靠信号。这就是”不可靠信号”的来源。Linux下的不可靠信号问题主要指的是信号可能丢失。
  2. 可靠信号:原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

注:可靠信号是指后来添加的新信号(信号值位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。
对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数(对所有信号这一点都成立),而经过signal安装的信号却不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

signal安装响应函数

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

void ouch(int sig)  
{  
    printf("oh, got a signal %d\n", sig);  

    int i = 0;  
    for (i = 0; i < 5; i++)  
    {  
        printf("signal func %d\n", i);  
        sleep(1);  
    }

    // (void) signal(SIGINT, SIG_DFL);  
     //(void) signal(SIGINT, ouch);  

 }  

int main()  { 

    (void) signal(SIGINT, ouch);  

    while(1)  {  

        printf("hello world...\n");  
        sleep(1);  
    }  
}  

连续按Ctrl+C时会发现 signal函数会堵塞当前正在处理的signal,但是没有办法阻塞其它signal,比如正在处理SIG_INT,再来一个SIG_INT则会堵塞,但是来SIG_QUIT则会被其中断。

sigaction安装响应函数

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

void ouch(int sig)  {  
    printf("oh, got a signal %d\n", sig);  

    int i = 0;  
    for (i = 0; i < 5; i++)  
    {  
        printf("signal func %d\n", i);  
        sleep(1);  
    }  
}  

int main()  
{  
    struct sigaction act;  
    act.sa_handler = ouch;  
    sigemptyset(&act.sa_mask);  
    sigaddset(&act.sa_mask, SIGQUIT);  
    // act.sa_flags = SA_RESETHAND;  
    // act.sa_flags = SA_NODEFER;  
    act.sa_flags = 0;  

    sigaction(SIGINT, &act, 0);  

    struct sigaction act_2;  
    act_2.sa_handler = ouch;  
    sigemptyset(&act_2.sa_mask);  
    act.sa_flags = 0;  
    sigaction(SIGQUIT, &act_2, 0);  

        while(1)  {  
             sleep(1);  
        }  

    return;  

    }  
  1. 阻塞,sigaction函数有阻塞的功能,比如SIGINT信号来了,进入信号处理函数,默认情况下,在信号处理函数未完成之前,如果又来了一个SIGINT信号,其将被阻塞,只有信号处理函数处理完毕,才会对后来的SIGINT再进行处理,同时后续无论来多少个SIGINT,仅处理一个SIGINT,sigaction会对后续SIGINT进行排队合并处理。

  2. sa_mask,信号屏蔽集,可以通过函数sigemptyset/sigaddset等来清空和增加需要屏蔽的信号,上面代码中,对信号SIGINT处理时,如果来信号SIGQUIT,其将被屏蔽,但是如果在处理SIGQUIT,来了SIGINT,则首先处理SIGINT,然后接着处理SIGQUIT。

  3. sa_flags如果取值为0,则表示默认行为。还可以取如下俩值,但是我没觉得这俩值有啥用。
    SA_NODEFER,如果设置来该标志,则不进行当前处理信号到阻塞
    SA_RESETHAND,如果设置来该标志,则处理完当前信号后,将信号处理函数设置为SIG_DFL行为。

信号的实际使用

MTK8685平台有mtk的 thermal管理,实时监控平台的温度,当主芯片的温度过高时,会提示用户cooldown 即重启盒子降温,以免损坏! 温度监控系统也是采用信号的方式实现的。thermald.c 首先设置响应的信号,并设置signal_handler,最终会将此进程的pid通过proc文件系统传递给内核! 内核的驱动检测到温度过高时,会将信号发送给指定的用户态程序,由用户决定下一步的操作!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <cutils/xlog.h>
#include <sys/types.h>

static int debug_on = 0;

#define TM_LOG_TAG "thermald"
#define TM_DBG_LOG(_fmt_, args...) \
    do { \
        if (1 == debug_on) { \
            sxlog_printf(ANDROID_LOG_INFO, TM_LOG_TAG, _fmt_, ##args); \
        } \
    } while(0)

#define TM_INFO_LOG(_fmt_, args...) \
    do { sxlog_printf(ANDROID_LOG_INFO, TM_LOG_TAG, _fmt_, ##args); } while(0)

#define PROCFS_MTK_CL_SD_PID "/proc/driver/mtk_cl_sd_pid"

static void signal_handler(int signo, siginfo_t *si, void *uc)
{
    switch(si->si_signo) {
        // Add more signo or code to expand thermald
        case SIGIO:
            if(1 == si->si_code) {
            //待收到高温警告后,启动提示对话框
                system("am start com.mediatek.thermalmanager/.ShutDownAlertDialogActivity");
                TM_INFO_LOG("thermal shutdown signal received, si_signo=%d, si_code=%d\n", si->si_signo, si->si_code);
            }
        break;
        default:
            TM_INFO_LOG("what!!!\n");
        break;
    }
}

int main(int argc, char *argv[])
{
    int fd = open(PROCFS_MTK_CL_SD_PID, O_RDWR);
    int pid = getpid();
    int ret = 0;
    char pid_string[32] = {0};

    struct sigaction act;

    TM_INFO_LOG("START+++++++++ %d", getpid());

    /* Create signal handler */
    memset(&act, 0, sizeof(act));
    act.sa_flags = SA_SIGINFO;//发送额外的信息给signal_handler
    //act.sa_handler = signal_handler;
    act.sa_sigaction = signal_handler;
    sigemptyset(&act.sa_mask);

    sigaction(SIGIO, &act, NULL);

    /* Write pid to procfs */
    sprintf(pid_string, "%d", pid);

    ret = write(fd, pid_string, sizeof(char) * strlen(pid_string)); //将当前的进程pid写入proc文件系统,供内核使用
    if (ret <= 0)   {
        TM_INFO_LOG("Fail to write %d to %s %x\n", pid, PROCFS_MTK_CL_SD_PID, ret);
    } else {
        TM_INFO_LOG("Success to write %d to %s\n", pid, PROCFS_MTK_CL_SD_PID);
    }
    close(fd);

    TM_INFO_LOG("Enter infinite loop");

    while(1) {
        sleep(100);
    }

    TM_INFO_LOG("END-----------");

    return 0;
}

下面看看内核是如何将信号发送到指定的用户程序的,相关代码如下:

驱动在初始化时会创建proc文件节点:
static int __init mtk_cooler_shutdown_init(void){    
    ......
    entry = proc_create("driver/mtk_cl_sd_pid", S_IRUGO | S_IWUSR | S_IWGRP, NULL, &_cl_sd_pid_fops);
    .....
    }

_cl_sd_pid_fops定义如下:
static const struct file_operations _cl_sd_pid_fops = {
    .owner = THIS_MODULE,
    .open = _mtk_cl_sd_pid_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .write = _mtk_cl_sd_pid_write,
    .release = single_release,
};
用户态下write  /proc/driver/mtk_cl_sd_pid文件时,kernel会调用_mtk_cl_sd_pid_write。_mtk_cl_sd_pid_write实现如下:
static ssize_t _mtk_cl_sd_pid_write(struct file *filp, const char __user *buf, size_t len, loff_t *data)
{
    int ret = 0;
    char tmp[MAX_LEN] = {0};

    /* write data to the buffer */
    if ( copy_from_user(tmp, buf, len) ) {
        return -EFAULT;
    }

    ret = kstrtouint(tmp, 10, &tm_input_pid);//通过proc文件系统,用户程序吧pid悄悄滴传送到了内核
    if (ret)
        WARN_ON(1);

    mtk_cooler_shutdown_dprintk("[%s] %s = %d\n", __func__, tmp, tm_input_pid);

    return len;
}

static int _mtk_cl_sd_send_signal(void)
{
    int ret = 0;

    if (tm_input_pid == 0) {
        mtk_cooler_shutdown_dprintk("[%s] pid is empty\n", __func__);
        ret = -1;
    }

    mtk_cooler_shutdown_dprintk("[%s] pid is %d, %d\n", __func__, tm_pid, tm_input_pid);

    if (ret == 0 && tm_input_pid != tm_pid) {
        tm_pid = tm_input_pid;
        pg_task = get_pid_task(find_vpid(tm_pid), PIDTYPE_PID);//根据pid获取task_struct结构体
    }

    if (ret == 0 && pg_task) {
        siginfo_t info;
        info.si_signo = SIGIO;
        info.si_errno = 0;
        info.si_code = 1;
        info.si_addr = NULL;
        ret = send_sig_info(SIGIO, &info, pg_task);//将SIGIO信号发送给指定的用户进程
    }

    if (ret != 0) mtk_cooler_shutdown_dprintk("[%s] ret=%d\n", __func__, ret);

    return ret;
}

源码路径mediatek/kernel/drivers/thermal/mtk_cooler_shutdown.c

较详细的linux 信号参考资料:
Linux环境进程间通信(二): 信号(上)
Linux环境进程间通信(二): 信号(下)
Linux signal那些事儿
Linux signal 那些事儿(2)
Linux signal 那些事儿 (3)

Linux系统中,信号量(Semaphore)是一种用于进程间同步的机制,特别是在多进程环境中,它能够有效地避免多个进程同时访问共享资源而导致的竞争条件。信号量的核心思想是通过计数器来控制对共享资源的访问,确保在任意时刻只有一个进程可以操作共享资源。 ### 信号量的基本操作 信号量的操作主要包括**P操作(等待)**和**V操作(发送)**: - **P操作**:如果信号量的值大于0,则将其减1;如果值为0,则进程进入等待状态,直到信号量的值变为正数。 - **V操作**:将信号量的值加1,并唤醒一个等待的进程(如果有)。 这些操作通常是通过`semop()`函数实现的,该函数用于执行对信号量集的操作。`semop()`的原型如下: ```c int semop(int semid, struct sembuf *sops, size_t nsops); ``` 其中: - `semid` 是通过`semget()`获取的信号量集标识符。 - `sops` 是一个`struct sembuf`结构体数组,每个元素描述了一个操作。 - `nsops` 表示要执行的操作数量。 `struct sembuf`的定义如下: ```c struct sembuf { unsigned short sem_num; // 信号量在集合中的索引 short sem_op; // 操作类型(+1, -1, 或 0) short sem_flg; // 操作标志(如 IPC_NOWAIT) }; ``` ### 创建和获取信号量集 使用`semget()`函数可以创建一个新的信号量集或获取一个已存在的信号量集。其函数原型如下: ```c int semget(key_t key, int nsems, int semflg); ``` - `key` 是信号量集的键值,通常通过`ftok()`函数生成。 - `nsems` 是信号量集中信号量的数量。 - `semflg` 是操作标志,常见的标志包括: - `IPC_CREAT`:如果信号量集不存在,则创建它。 - `IPC_EXCL`:与`IPC_CREAT`一起使用,确保返回一个新的信号量集。 - `0666`:设置信号量集的权限。 例如,创建一个包含1个信号量的信号量集: ```c key_t key = ftok("/tmp", 'A'); // 生成一个唯一的键值 int semid = semget(key, 1, IPC_CREAT | 0666); // 创建信号量集 if (semid == -1) { perror("semget failed"); exit(EXIT_FAILURE); } ``` ### 初始化信号量 创建信号量集后,需要使用`semctl()`函数对其进行初始化。`semctl()`的原型如下: ```c int semctl(int semid, int semnum, int cmd, ...); ``` - `semid` 是信号量集的标识符。 - `semnum` 是信号量在集合中的索引。 - `cmd` 是控制命令,常见的命令包括: - `SETVAL`:设置信号量的初始值。 - `IPC_RMID`:删除信号量集。 例如,将信号量初始化为1: ```c union semun { int val; // 用于 SETVAL struct semid_ds *buf; // 用于 IPC_STAT 和 IPC_SET unsigned short *array; // 用于 GETALL 和 SETALL } arg; arg.val = 1; // 设置初始值为1 if (semctl(semid, 0, SETVAL, arg) == -1) { perror("semctl failed"); exit(EXIT_FAILURE); } ``` ### 使用信号量进行同步 假设我们有两个进程,它们都需要访问共享内存中的某个资源。为了确保同一时间只有一个进程可以访问该资源,我们可以使用信号量进行同步。 #### 示例代码 以下是两个进程使用信号量同步访问共享内存的示例代码。 ##### 进程1(生产者) ```c #include <stdio.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include <string.h> // 定义共享内存的大小 #define SHM_SIZE 1024 // 定义信号量操作结构体 struct sembuf sem_op; // 定义共享内存的键值 key_t key = 1234; int main() { int shmid, semid; char *data; // 创建共享内存 shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666); if (shmid == -1) { perror("shmget failed"); exit(EXIT_FAILURE); } // 将共享内存连接到当前进程的地址空间 data = shmat(shmid, NULL, 0); if (data == (char *)-1) { perror("shmat failed"); exit(EXIT_FAILURE); } // 创建信号量集 semid = semget(key, 1, IPC_CREAT | 0666); if (semid == -1) { perror("semget failed"); exit(EXIT_FAILURE); } // 初始化信号量为1 union semun arg; arg.val = 1; if (semctl(semid, 0, SETVAL, arg) == -1) { perror("semctl failed"); exit(EXIT_FAILURE); } // 执行P操作(等待) sem_op.sem_num = 0; sem_op.sem_op = -1; sem_op.sem_flg = 0; if (semop(semid, &sem_op, 1) == -1) { perror("semop failed"); exit(EXIT_FAILURE); } // 写入数据到共享内存 strcpy(data, "Hello from producer!"); // 执行V操作(释放) sem_op.sem_op = 1; if (semop(semid, &sem_op, 1) == -1) { perror("semop failed"); exit(EXIT_FAILURE); } // 分离共享内存 if (shmdt(data) == -1) { perror("shmdt failed"); exit(EXIT_FAILURE); } return 0; } ``` ##### 进程2(消费者) ```c #include <stdio.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> // 定义共享内存的大小 #define SHM_SIZE 1024 // 定义信号量操作结构体 struct sembuf sem_op; // 定义共享内存的键值 key_t key = 1234; int main() { int shmid, semid; char *data; // 获取共享内存 shmid = shmget(key, SHM_SIZE, 0666); if (shmid == -1) { perror("shmget failed"); exit(EXIT_FAILURE); } // 将共享内存连接到当前进程的地址空间 data = shmat(shmid, NULL, 0); if (data == (char *)-1) { perror("shmat failed"); exit(EXIT_FAILURE); } // 获取信号量集 semid = semget(key, 1, 0666); if (semid == -1) { perror("semget failed"); exit(EXIT_FAILURE); } // 执行P操作(等待) sem_op.sem_num = 0; sem_op.sem_op = -1; sem_op.sem_flg = 0; if (semop(semid, &sem_op, 1) == -1) { perror("semop failed"); exit(EXIT_FAILURE); } // 读取共享内存中的数据 printf("Data from shared memory: %s\n", data); // 执行V操作(释放) sem_op.sem_op = 1; if (semop(semid, &sem_op, 1) == -1) { perror("semop failed"); exit(EXIT_FAILURE); } // 分离共享内存 if (shmdt(data) == -1) { perror("shmdt failed"); exit(EXIT_FAILURE); } return 0; } ``` ### 常见问题 1. **信号量未初始化**:在使用信号量之前,必须确保其已经被正确初始化。否则,可能会导致进程无法正确进入等待状态或释放信号量。 2. **信号量操作失败**:如果`semop()`调用失败,应该检查错误码并采取适当的措施,例如重新尝试操作或退出程序。 3. **信号量泄漏**:在程序结束时,必须确保释放所有分配的信号量资源。可以使用`semctl()`的`IPC_RMID`命令删除信号量集。 4. **权限问题**:创建信号量时,必须确保具有足够的权限。如果权限不足,可能会导致`semget()`失败。 5. **竞争条件**:即使使用了信号量,仍然需要注意其他可能的竞争条件,例如多个进程同时修改共享内存中的数据结构。 ### 总结 信号量是Linux IPC中非常重要的同步机制,能够有效避免多个进程同时访问共享资源而导致的竞争条件。通过`semget()`、`semctl()`和`semop()`等函数,可以方便地创建、管理和操作信号量集。在实际应用中,需要注意信号量的初始化、操作和清理,以确保程序的稳定性和可靠性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值