Linux -- 进程间通信的五种方式

IPC(InterProcess Communication)的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中Socket和Stream支持不同主机上的两个进程IPC。

  1. 管道(Pipes):管道是一种半双工的通信方式,用于具有亲缘关系的进程间通信。它通常用于父子进程或者兄弟进程之间。管道可以是匿名管道,也可以是命名管道。

  2. 消息队列(Message Queues):消息队列是一种通过消息传递进行通信的方式。发送方将消息发送到队列中,接收方从队列中接收消息。消息队列可以实现进程间的异步通信。

  3. 信号量(Semaphores):信号量是一种计数器,用于控制对共享资源的访问。它通常用于同步进程之间的操作,以避免竞争条件。

  4. 共享内存(Shared Memory):共享内存是一种允许多个进程访问同一块内存区域的方式。这种方式通常比较高效,但需要处理进程间的同步和互斥。

  5. 套接字(Sockets):套接字是一种网络编程接口,不仅可以用于不同主机间的进程通信,也可以用于同一主机上的进程通信。套接字可以基于网络协议(如TCP/IP)或本地协议(如UNIX域套接字)实现。

一、管道

管道(Pipes)是一种在Unix和类Unix系统中常见的进程间通信(IPC)机制,用于在具有亲缘关系的进程之间传递数据。管道是一个单向通道,允许一个进程将输出直接发送到另一个进程的输入。它是一种半双工通信方式,即数据只能单向流动,不能双向传输。

类型

  1. 匿名管道(Anonymous Pipes):匿名管道是最简单的管道形式,它只存在于内存中,并且通常用于父子进程之间的通信。在Unix系统中,可以使用pipe()系统调用创建匿名管道。

  2. 命名管道(Named Pipes):命名管道是一种具有持久性的管道,它以文件的形式存在于文件系统中,并允许无关进程之间进行通信。命名管道通常用于不具有亲缘关系的进程之间的通信。

特点

  • 单向通信:管道是单向的,数据只能沿着管道的方向流动,不能双向传输。

  • 半双工:管道是半双工的,即数据只能在一个方向上传输。如果需要双向通信,通常需要创建两个管道。

  • FIFO(先进先出):管道遵循FIFO的原则,即数据按照写入的顺序从管道中读取出来。

使用

在Unix系统中,可以使用pipe()系统调用创建匿名管道,它返回两个文件描述符,一个用于读取,一个用于写入。然后可以使用fork()创建一个新的进程,在父子进程之间共享管道,并使用dup2()系统调用将管道文件描述符重定向到标准输入或标准输出。接着,一个进程可以通过写入管道的方式向另一个进程发送数据,另一个进程则可以通过读取管道来接收数据。

示例

下面是一个简单的C语言示例,演示了如何在父子进程之间使用匿名管道进行通信:

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

int main()
{
        int fd[2];

        int pid;

        char buf[128];

        //创建管道
        if(pipe(fd) == -1) { 
                printf("creat pipe failed\n");
        }

        //创建子进程
        pid = fork();

        if(pid < 0) {
                printf("creat child faild\n");
        } else if(pid > 0) {    //父进程
                printf("this is father\n");
                close(fd[0]); //关闭读取端

                //向管道写数据
                write(fd[1], "hello from father", strlen("hello from father"));

                wait(NULL);
        } else {
                printf("this is child\n");
                close(fd[1]); //关闭写入端
                read(fd[0], buf, sizeof(buf));  //从管道读数据
                printf("child print: %s\n", buf);
                exit(1);
        }


        return 0;
}

程序执行结果如下: 

命名管道(Named Pipes)的使用:

命名管道是一种具有持久性的管道,它以文件的形式存在于文件系统中,并允许无关进程之间进行通信。相比于匿名管道,命名管道允许不具有亲缘关系的进程之间进行通信。

创建命名管道

在Unix/Linux系统中,可以使用mkfifo()函数创建命名管道。命名管道创建后,会在文件系统中生成一个特殊类型的文件,它可以像普通文件一样被打开、读取和写入。

使用命名管道

使用命名管道和使用普通文件一样,可以使用文件I/O操作来读取和写入数据。不同的是,命名管道的数据读取和写入是以先进先出(FIFO)的方式进行的,即写入的数据按照写入的顺序从管道中读取出来。

特点

  • 命名管道是持久性的,创建后会一直存在于文件系统中,直到被显式删除。
  • 允许不具有亲缘关系的进程之间进行通信。
  • 数据按照写入的顺序从管道中读取出来,具有先进先出(FIFO)的特性。

示例

下面是一个简单的C语言示例,演示了如何创建和使用命名管道:

先介绍一下mkfifo:mkfifo 是用于创建命名管道(FIFO)的系统调用。在 Unix 和类 Unix 系统中,命名管道以文件的形式存在,可以用于不同进程之间进行通信。

mkfifo 函数原型

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

int mkfifo(const char *pathname, mode_t mode);
参数
  • pathname:要创建的命名管道的路径。
  • mode:管道的权限,类似于 openchmod 中使用的权限位。常用权限包括 0666(表示管道文件可读可写)。
返回值
  • 成功时返回 0
  • 失败时返回 -1,并设置 errno 以指示错误。

 注意:

命名管道的读写操作是同步的,这意味着:

  • 写入进程会等待直到有读取进程打开管道进行读取。
  • 读取进程会等待直到有写入进程向管道写入数据。

这导致如果你先运行写入程序而没有相应的读取程序在运行,写入程序会阻塞,等待读取程序打开管道读取数据。同样地,如果你先运行读取程序而没有写入程序在运行,读取程序会阻塞,等待写入程序向管道写入数据。

为了避免这个问题,可以按以下步骤运行程序:

  1. 先运行读取程序 reader,使其准备好从管道读取数据。
  2. 然后运行写入程序 writer,向管道写入数据。
writer.c (写入程序)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_FILE "/tmp/my_fifo"

int main() {
    // 创建命名管道,权限模式为 0600
    if (mkfifo(FIFO_FILE, 0600) == -1) {
        perror("mkfifo failed");
        exit(EXIT_FAILURE);
    }

    // 打开命名管道以写入数据
    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd == -1) {
        perror("open failed");
        exit(EXIT_FAILURE);
    }

    // 写入数据到命名管道
    const char *message = "Hello, Named Pipe!";
    write(fd, message, sizeof(message));

    // 关闭文件描述符
    close(fd);

    return 0;
}
reader.c (读取程序)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_FILE "/tmp/my_fifo"

int main() {
    char buffer[BUFSIZ];

    // 打开命名管道以读取数据
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd == -1) {
        perror("open failed");
        exit(EXIT_FAILURE);
    }

    // 从命名管道读取数据
    read(fd, buffer, sizeof(buffer));
    printf("Received: %s\n", buffer);

    // 关闭文件描述符
    close(fd);

    // 删除命名管道文件
    unlink(FIFO_FILE);

    return 0;
}

程序运行结果:

 

在Linux中,消息队列是进程间通信(IPC)的一种机制,它允许进程通过发送和接收消息进行通信。消息队列是系统V IPC的一部分,并且提供了一种高效且灵活的方式来在独立进程之间传递信息。

二、消息队列

消息队列的基本概念

  • 消息队列:是内核维护的一个链表,存储着消息。每个消息队列都有一个唯一的标识符(队列ID)。
  • 消息:每个消息包含一个类型(长整型)和一个数据部分(字节数组)。
  • 消息类型:用于区分不同类型的消息,接收进程可以根据消息类型选择接收特定类型的消息。

消息队列的关键操作

消息队列的操作主要包括创建、发送、接收和控制。以下是这些操作对应的系统调用和它们的基本用法。

创建或获取消息队列

使用 msgget 函数创建或获取一个消息队列标识符。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

参数说明

  • key:消息队列的键值。通过这个键值可以唯一标识一个消息队列。可以使用 ftok 函数生成。
  • msgflg:消息队列的标志,可以是以下标志的组合:
    • IPC_CREAT:如果消息队列不存在,则创建一个新的消息队列。
    • IPC_EXCL:与 IPC_CREAT 一起使用时,如果消息队列已经存在,则返回错误。
    • 权限标志:如 0666,表示消息队列的访问权限。
返回值
  • 成功时返回消息队列的标识符(非负整数)。
  • 失败时返回 -1,并设置 errno 以指示错误类型。

 

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msqid:消息队列标识符。
  • msgp:指向消息结构体的指针。消息结构体的定义如下:
    struct msgbuf {
        long mtype;       // 消息类型
        char mtext[1];    // 消息正文
    };
    
  • msgsz:消息正文的大小(不包括类型)。
  • msgflg:操作标志(如 IPC_NOWAIT)。
接收消息

使用 msgrcv 函数从消息队列接收消息。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • msqid:消息队列标识符。
  • msgp:指向消息结构体的指针。
  • msgsz:消息正文的大小。
  • msgtyp:消息类型,指定要接收的消息类型。
  • msgflg:操作标志(如 IPC_NOWAITMSG_NOERROR)。
控制消息队列

使用 msgctl 函数对消息队列进行控制操作,如删除消息队列或获取消息队列的状态。

  • msqid:消息队列标识符。
  • cmd:控制命令(如 IPC_RMID 删除消息队列,IPC_STAT 获取消息队列状态,IPC_SET 设置消息队列属性)。
  • buf:指向 msqid_ds 结构体的指针,用于存储或设置消息队列的属性。

ftok 函数

ftok 函数的原型如下:

#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
  • pathname:一个现有文件的路径名。通常,这个文件必须存在且可以访问。
  • proj_id:一个项目标识符,是一个字符(通常是一个整数值),用于生成键值的补充标识。

ftok 的工作原理

ftok 函数通过组合 pathnameproj_id 生成一个唯一的键值(key_t 类型)。

为什么使用 ftok

  • 唯一性:确保在同一系统中不同的IPC对象可以通过不同的键值唯一标识。
  • 可重复性:只要 pathnameproj_id 保持不变,每次调用 ftok 函数生成的键值是相同的。

示例代码

以下是一个完整的示例,展示了如何创建消息队列、发送消息、接收消息并删除消息队列。

示例说明

  • 进程A:发送消息到消息队列。
  • 进程B:从消息队列接收消息。

进程A:发送消息

首先,编写一个程序作为发送消息的进程A。

// sender.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf {
    long mtype;
    char mtext[100];
};

int main() {
    key_t key;
    int msgid;
    struct msgbuf message;

    // 生成唯一键
    key = ftok("msgqueuefile", 65);
    if (key == -1) {
        perror("ftok failed");
        exit(1);
    }

    // 创建消息队列
    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget failed");
        exit(1);
    }

    // 准备消息
    message.mtype = 1;  // 消息类型
    strcpy(message.mtext, "Hello from Process A!");

    // 发送消息
    if (msgsnd(msgid, &message, sizeof(message.mtext), 0) == -1) {
        perror("msgsnd failed");
        exit(1);
    }

    printf("Message sent: %s\n", message.mtext);

    return 0;
}

进程B:接收消息

然后,编写另一个程序作为接收消息的进程B。

// receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>

struct msgbuf {
    long mtype;
    char mtext[100];
};

int main() {
    key_t key;
    int msgid;
    struct msgbuf message;

    // 生成唯一键
    key = ftok("msgqueuefile", 65);
    if (key == -1) {
        perror("ftok failed");
        exit(1);
    }

    // 获取消息队列标识符
    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget failed");
        exit(1);
    }

    // 接收消息类型为 1 的消息
    if (msgrcv(msgid, &message, sizeof(message.mtext), 1, 0) == -1) {
        perror("msgrcv failed");
        exit(1);
    }

    printf("Received message: %s\n", message.mtext);

    // 删除消息队列(可选操作,一般在所有进程完成通信后由一个进程执行)
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl (IPC_RMID) failed");
        exit(1);
    }

    return 0;
}

解释

  1. 生成键值:两个进程使用相同的键值生成方法(ftok),确保它们访问同一个消息队列。
  2. 创建/获取消息队列
    • 在发送进程中,消息队列不存在时创建消息队列(IPC_CREAT)。
    • 在接收进程中,获取消息队列标识符。如果队列不存在时创建它(IPC_CREAT)。
  3. 发送消息:发送进程将消息发送到消息队列。
  4. 接收消息:接收进程从消息队列中接收消息,并根据消息类型进行过滤(这里消息类型为1)。
  5. 删除消息队列:接收进程接收完消息后删除消息队列(msgctl),这是可选的,一般由一个进程在通信完成后执行。

三、共享内存

共享内存是一种高效的进程间通信(IPC)机制,它允许多个进程共享一段内存。这种方式比消息队列更高效,因为数据只需要在内存中复制一次。下面是使用共享内存的一个示例,包括创建共享内存段、附加到共享内存段、写入和读取数据,以及删除共享内存段。

比喻解释

假设你和你的朋友们在一个房间里合作完成一个项目。房间中间有一块黑板,你们都可以在上面写字、擦字和读内容。黑板就是共享内存,而你和你的朋友们就是不同的进程。

  • 共享内存:黑板,所有人(进程)都可以看到和操作。
  • 进程:你和你的朋友们,每个人都可以对黑板进行读写操作。

使用共享内存的步骤

  1. 创建或获取共享内存段。
  2. 将共享内存段附加到进程的地址空间。
  3. 访问共享内存(读写数据)。
  4. 分离共享内存段。
  5. 删除共享内存段(可选)。

共享内存函数概述

  1. shmget - 创建或获取一个共享内存段。
  2. shmat - 将共享内存段附加到进程的地址空间。
  3. shmdt - 将共享内存段从进程的地址空间分离。
  4. shmctl - 控制共享内存段,包括删除共享内存段。

 shmget 函数

int shmget(key_t key, size_t size, int shmflg);
  • 参数

    • key:共享内存段的键值。通常使用 ftok 生成。
    • size:共享内存段的大小(字节)。
    • shmflg:权限标志和选项。常见值包括 0666(读写权限)和 IPC_CREAT(如果共享内存段不存在则创建)。
  • 返回值

    • 成功时返回共享内存段的标识符(非负整数)。
    • 失败时返回 -1,并设置 errno

 shmat 函数

void* shmat(int shmid, const void *shmaddr, int shmflg);
  • 参数

    • shmid:共享内存段的标识符,由 shmget 返回。
    • shmaddr:希望附加的地址,通常为 NULL,表示由系统决定。
    • shmflg:选项标志,通常为 0
  • 返回值

    • 成功时返回指向共享内存段的指针。
    • 失败时返回 (void *)-1,并设置 errno

shmdt 函数

int shmdt(const void *shmaddr);
  • 参数

    • shmaddr:共享内存段的地址,由 shmat 返回。
  • 返回值

    • 成功时返回 0
    • 失败时返回 -1,并设置 errno

shmctl 函数

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 参数

    • shmid:共享内存段的标识符。
    • cmd:控制命令,如 IPC_RMID(删除共享内存段)、IPC_STAT(获取共享内存段的状态)、IPC_SET(设置共享内存段的状态)。
    • buf:用于存储或传递共享内存段信息的结构体,通常用于 IPC_STATIPC_SET 命令。
  • 返回值

    • 成功时返回 0
    • 失败时返回 -1,并设置 errno

代码示例

下面是一个简单的示例,展示如何使用共享内存进行进程间通信。

 共享内存头文件(common.h)

#ifndef COMMON_H
#define COMMON_H

#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024  // 共享内存大小
#define SHM_KEY  1234  // 共享内存键值

#endif // COMMON_H
写入共享内存的进程(writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "common.h"

int main() {
    int shmid;
    char *shmaddr;

    // 创建共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    // 将共享内存段附加到进程的地址空间
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat failed");
        exit(1);
    }

    // 向共享内存写入数据
    strncpy(shmaddr, "Hello from writer!", SHM_SIZE);

    printf("Data written to shared memory: %s\n", shmaddr);

    // 分离共享内存段
    if (shmdt(shmaddr) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    return 0;
}
读取共享内存的进程(reader.c)
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main() {
    int shmid;
    char *shmaddr;

    // 获取共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    // 将共享内存段附加到进程的地址空间
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat failed");
        exit(1);
    }

    // 读取共享内存中的数据
    printf("Data read from shared memory: %s\n", shmaddr);

    // 分离共享内存段
    if (shmdt(shmaddr) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    // 删除共享内存段(可选操作,一般在所有进程完成通信后由一个进程执行)
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl (IPC_RMID) failed");
        exit(1);
    }

    return 0;
}

通过共享内存,不同进程可以非常高效地共享数据,就像大家一起在一个黑板上写字和读字一样。这种机制特别适合需要快速、大量数据交换的场景,比如多进程计算、实时数据处理等。

四、信号

在Linux中,信号是一种进程间通信(IPC)的机制,用于通知进程发生了某种事件。信号可以用于通知进程硬件异常、用户请求的中断、操作系统事件等。 

常见的Linux信号

一些常见的Linux信号及其含义:

信号编号信号名称描述
1SIGHUP终端挂起或控制进程终止
2SIGINT来自键盘的中断(Ctrl+C)
3SIGQUIT来自键盘的退出(Ctrl+\)
9SIGKILL无条件终止进程(不能捕捉、阻塞或忽略)
15SIGTERM终止进程(可以捕捉、阻塞或处理)
17,19,23SIGCHLD子进程结束
18,20,22SIGTSTP来自键盘的停止(Ctrl+Z)
19,21,23SIGCONT继续执行一个停止的进程
比喻解释

假设你在一个图书馆里读书(模拟一个进程正在执行任务),图书馆里有一个广播系统(相当于信号机制)。图书馆管理员(操作系统)可以通过广播系统向所有读者发送不同类型的信息(信号)。

  • 警报信号(SIGINT):广播系统通知所有人立即离开图书馆(相当于按 Ctrl+C 终止进程)。
  • 提示信号(SIGUSR1):广播系统通知某个读者有人找他(特定信号,应用程序可以自定义处理)。
  • 关门信号(SIGTERM):广播系统通知图书馆将在10分钟后关闭,大家需要收拾东西准备离开(请求程序优雅地终止)。
  • 火警信号(SIGKILL):广播系统发出紧急疏散警报,要求所有人立刻离开(强制终止进程)。

信号的处理

进程可以处理信号的方式有三种:

  1. 默认处理:操作系统为每个信号定义了默认的处理方式。
  2. 忽略信号:进程可以选择忽略某些信号。
  3. 自定义处理:进程可以捕捉信号并执行自定义的信号处理程序。

相关函数

  1. signal 函数:用于指定信号的处理程序。
  2. kill 函数:用于向进程发送信号。
  3. raise 函数:用于向当前进程发送信号。
  4. alarm 函数:用于设置一个定时器,当定时器到期时发送 SIGALRM 信号。
  5. pause 函数:挂起进程,直到捕获到信号。

signal 函数是用来设置一个进程的信号处理程序的。 

函数原型 

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

参数说明

  • signum:要捕捉或处理的信号编号。例如,SIGINT 是中断信号,通常是用户按 Ctrl+C 触发的信号。
  • handler:信号处理程序,可以是以下三种之一:
    • 一个指向信号处理函数的指针。当信号到达时,该函数将被调用。
    • SIG_IGN:表示忽略该信号。
    • SIG_DFL:表示使用默认的信号处理方式。
返回值
  • 成功时,返回前一个信号处理函数的地址。
  • 失败时,返回 SIG_ERR,并设置 errno 以指示错误。

示例代码

以下是一个示例程序,它捕捉 SIGINT 信号,并在信号到达时执行特定的处理逻辑:

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

// 信号处理函数
void handle_sigint(int sig) {
    printf("Caught signal %d (SIGINT). Exiting...\n", sig);
    exit(0);
}

int main() {
    // 设置信号处理函数
    if (signal(SIGINT, handle_sigint) == SIG_ERR) {
        perror("signal");
        exit(1);
    }

    printf("Press Ctrl+C to trigger SIGINT...\n");

    // 无限循环,等待信号
    while (1) {
        sleep(1);
    }

    return 0;
}

sigactionsignal 都是用于设置信号处理程序的函数,但 sigaction 提供了更多的功能和更高的可靠性。以下是它们的主要区别:

功能与特性

signal 函数
  • 简单易用signal 函数的接口较简单,适用于基本的信号处理需求。
  • 可移植性差:在不同的操作系统和系统版本之间,signal 的行为可能有所不同。
  • 缺乏高级特性:不支持额外的标志位和信号处理选项,无法传递额外的上下文信息。
sigaction 函数
  • 高级功能:提供更多的控制选项,包括额外的标志位和信号处理选项。
  • 可靠性高:在各种操作系统和系统版本之间行为一致,提供更高的可移植性。
  • 丰富的信号信息:能够接收和处理更多的信号上下文信息(如信号发送者的进程ID,信号附带的值等)。
  • 支持信号排队:在使用实时信号时,支持信号排队,避免信号丢失。

 sigaction 函数

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:要捕捉或处理的信号编号。
  • act:指向包含新信号处理程序信息的 struct sigaction 结构体。
  • oldact:指向存储旧信号处理程序信息的 struct sigaction 结构体,如果不需要可以设为 NULL

struct sigaction 结构体

struct sigaction {
    void (*sa_handler)(int);           // 信号处理函数指针或常量 SIG_IGN, SIG_DFL
    void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数,用于捕捉带附加信息的信号
    sigset_t sa_mask;                  // 在信号处理程序运行时需要屏蔽的信号集
    int sa_flags;                      // 标志位,用于设置信号处理选项
    void (*sa_restorer)(void);         // 已废弃,不再使用
};

 struct sigaction 是在标准库 <signal.h> 中预先声明和定义的。

字段说明

  • sa_handler:指向信号处理函数的指针,也可以是 SIG_IGN(忽略信号)或 SIG_DFL(使用默认处理)。
  • sa_sigaction:指向带有更多信息的信号处理函数的指针,只有在设置了 SA_SIGINFO 标志时才使用。
  • sa_mask:在处理信号时要阻塞的信号集。
  • sa_flags:控制信号处理的行为的标志,可以设置多个标志以启用不同的功能。
  • sa_restorer:已经废弃,不再使用。

常用标志

  • SA_SIGINFO:使用 sa_sigaction 处理函数,允许接收更多的信号信息。
  • SA_RESTART:使被信号中断的系统调用自动重启。
  • SA_NOCLDWAIT:防止子进程变为僵尸进程(用于 SIGCHLD 信号)。
  • SA_NODEFER:不自动屏蔽正在处理的信号。

代码示例

使用 sigaction 设置信号处理程序
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

// 信号处理函数
void handle_signal(int sig, siginfo_t *info, void *ucontext) {
    printf("Caught signal %d\n", sig);
    if (info != NULL) {
        printf("Signal originates from process %d\n", info->si_pid);
    }
    exit(0);
}

int main() {
    struct sigaction sa;

    // 设置信号处理函数
    sa.sa_flags = SA_SIGINFO;             // 使用 sa_sigaction 处理函数
    sa.sa_sigaction = handle_signal;      // 指定信号处理函数
    sigemptyset(&sa.sa_mask);             // 清空信号屏蔽集

    // 设置 SIGINT 的处理程序
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    printf("Press Ctrl+C to trigger SIGINT...\n");

    // 无限循环,等待信号
    while (1) {
        sleep(1);
    }

    return 0;
}

代码解释

struct sigaction sa;

声明一个 sigaction 结构体实例 sa,它将用于设置信号处理程序。

sa.sa_flags = SA_SIGINFO;

设置 sa_flags 成员,这个标志位决定信号处理的行为。SA_SIGINFO 标志表示使用 sa_sigaction 成员而不是 sa_handler 成员来处理信号。

sa.sa_sigaction = handle_signal;

sa_sigaction 成员设置为 handle_signal 函数指针,表示当捕获到信号时,调用这个信号处理函数。

sigemptyset(&sa.sa_mask);

初始化 sa_mask 成员,将其设置为空的信号集。这意味着在处理信号时,当前信号处理程序不会阻塞其他任何信号。

if (sigaction(SIGINT, &sa, NULL) == -1) { ... }

使用 sigaction 函数将 SIGINT 信号的处理程序设置为 handle_signal 函数。sigaction 函数的参数解释如下:

  • SIGINT:要捕获的信号。
  • &sa:指向包含信号处理程序设置的 sigaction 结构体。
  • NULL:指向一个 sigaction 结构体,用于保存之前的信号处理程序。如果不需要保存之前的处理程序,可以传递 NULL

未显式设置的成员

struct sigaction 结构体中,以下成员没有显式设置:

  • sa_handler:因为我们设置了 SA_SIGINFO 标志,所以使用 sa_sigaction 而不是 sa_handler。因此,这里不需要设置 sa_handler
  • sa_restorer:已经废弃,不再使用,通常忽略。
  • sa_mask:初始化为空信号集,这里显式地调用了 sigemptyset 来进行设置。sigemptyset 是一个用于初始化信号集的函数。它将指定的信号集中的所有信号清除,使信号集为空。这在设置信号处理程序时非常有用,因为它可以确保信号集从已知的空状态开始,然后可以添加特定的信号。

假设有一个运行中的进程无法响应 SIGTERM 或其他终止信号,例如一个陷入死循环或资源争用的进程。在这种情况下,可以使用 kill -9 来强制终止它。

  • kill -9 是一个命令行工具,用于发送 SIGKILL 信号,强制终止目标进程。
  • SIGKILL 信号无法被捕捉、阻塞或忽略,是确保进程终止的最后手段。
示例步骤

1.查看当前运行的进程

可以使用 ps 命令或其他进程查看工具(如 top)来获取进程 ID。

ps -aux | grep a.out

2.使用 kill -9 终止进程

假设我们要终止的进程 ID 是 1234,我们可以执行以下命令:

kill -9 1234

kill 函数

kill 函数用于向指定的进程或进程组发送信号。这个函数不仅可以发送终止信号,还可以发送其他类型的信号。

#include <signal.h>

int kill(pid_t pid, int sig);
参数
  • pid:要发送信号的进程ID。如果为正数,则表示单个进程的ID;如果为0,则表示发送信号给与调用进程在同一个进程组中的所有进程;如果为-1,则表示发送信号给系统中的所有进程(只有超级用户可以这样做);如果为负数,但不等于-1,则表示发送信号给进程组ID为-pid的所有进程。
  • sig:要发送的信号。
返回值
  • 成功时返回0。
  • 失败时返回-1,并设置errno以指示错误。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("Child process PID: %d\n", getpid());
        while (1) {
            sleep(1);
        }
    } else {
        // 父进程
        sleep(2); // 等待子进程启动
        printf("Sending SIGTERM to child process...\n");
        if (kill(pid, SIGTERM) == -1) {
            perror("kill");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}

sigqueue 函数

sigqueue 函数用于向指定的进程发送信号,并且可以携带附加数据。这对于需要传递额外信息的信号处理程序非常有用。

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);
参数
  • pid:要发送信号的进程ID。
  • sig:要发送的信号。
  • value:一个联合类型sigval,用于携带附加数据。
返回值
  • 成功时返回0。
  • 失败时返回-1,并设置errno以指示错误。

这段代码演示了如何使用信号处理函数 sigactionsigqueue 来发送和处理带有附加数据的信号。

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

// 信号处理函数
void handle_signal(int sig, siginfo_t *info, void *ucontext) {
    if (sig == SIGUSR1) {
        printf("Caught SIGUSR1 with value %d\n", info->si_value.sival_int);
    }
}

int main() {
    struct sigaction sa;
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handle_signal;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("Child process PID: %d\n", getpid());
        while (1) {
            sleep(1);
        }
    } else {
        // 父进程
        sleep(2); // 等待子进程启动
        union sigval value;
        value.sival_int = 42;
        printf("Sending SIGUSR1 to child process with value %d...\n", value.sival_int);
        if (sigqueue(pid, SIGUSR1, value) == -1) {
            perror("sigqueue");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}

代码运行结果:

以下是对代码的详细解释:

信号处理函数
void handle_signal(int sig, siginfo_t *info, void *ucontext) {
    if (sig == SIGUSR1) {
        printf("Caught SIGUSR1 with value %d\n", info->si_value.sival_int);
    }
}
  • handle_signal 是信号处理函数。
  • 参数:
    • sig:接收到的信号编号。
    • info:指向 siginfo_t 结构体的指针,包含了信号的附加信息。
    • ucontext:指向用户上下文的指针,通常在这个例子中不使用。
  • 当接收到 SIGUSR1 信号时,打印附带的数据 info->si_value.sival_int
设置信号处理程序
struct sigaction sa;
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = handle_signal;
sigemptyset(&sa.sa_mask);

if (sigaction(SIGUSR1, &sa, NULL) == -1) {
    perror("sigaction");
    exit(EXIT_FAILURE);
}
  • struct sigaction sa:声明 sigaction 结构体变量 sa
  • sa.sa_flags = SA_SIGINFO:设置 SA_SIGINFO 标志,使用 sa_sigaction 成员而不是 sa_handler
  • sa.sa_sigaction = handle_signal:指定信号处理函数 handle_signal
  • sigemptyset(&sa.sa_mask):初始化信号屏蔽集,清空信号集。
  • sigaction(SIGUSR1, &sa, NULL):设置 SIGUSR1 的处理程序。如果调用失败,打印错误信息并退出程序。
创建子进程
pid_t pid = fork();
if (pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
} else if (pid == 0) {
    // 子进程
    printf("Child process PID: %d\n", getpid());
    while (1) {
        sleep(1);
    }
} else {
    // 父进程
    sleep(2); // 等待子进程启动
    union sigval value;
    value.sival_int = 42;
    printf("Sending SIGUSR1 to child process with value %d...\n", value.sival_int);
    if (sigqueue(pid, SIGUSR1, value) == -1) {
        perror("sigqueue");
        exit(EXIT_FAILURE);
    }
}
  • pid_t pid = fork():创建子进程。
    • 如果 fork 失败,打印错误信息并退出程序。
    • 如果 pid == 0,这是在子进程中执行的代码:
      • 打印子进程的 PID。
      • 进入一个无限循环,每秒休眠一次。
    • 如果 pid > 0,这是在父进程中执行的代码:
      • 父进程等待 2 秒以确保子进程启动。
      • 创建一个 union sigval 变量 value 并设置 value.sival_int = 42
      • 打印发送信号的信息。
      • 使用 sigqueue 向子进程发送 SIGUSR1 信号,并附带整数值 42。如果发送失败,打印错误信息并退出程序。

信号发送和接收

  • 发送信号:父进程使用 sigqueue 发送 SIGUSR1 信号给子进程,并携带一个整数值 42
  • 接收信号:子进程在接收到 SIGUSR1 信号时,通过 handle_signal 函数处理,并打印接收到的信号值 42

siginfo_t 结构体

siginfo_t 的定义在 <signal.h> 头文件中。它的结构大致如下:

typedef struct siginfo {
    int      si_signo;    // 信号编号
    int      si_errno;    // 错误码(如果有的话)
    int      si_code;     // 信号的来源
    pid_t    si_pid;      // 发送信号的进程ID
    uid_t    si_uid;      // 发送信号的用户ID
    void    *si_addr;     // 相关地址(取决于信号)
    int      si_status;   // 子进程状态(对于 SIGCHLD 信号)
    int      si_band;     // 信号带(对于 SIGPOLL 信号)
    union sigval si_value; // 信号附带的值(对于实时信号)
} siginfo_t;
  • union sigval 是一个联合体,可以容纳不同类型的数据(整数和指针),使得信号处理函数可以灵活处理不同类型的附加数据。

总结

  • union sigval 是一个联合体,可以存储一个整数或一个指针,用于通过信号发送附加数据。
  • 在发送信号时,通过 sigqueue 函数将 union sigval 变量作为信号的一部分发送给目标进程。
  • 在信号处理函数中,通过 siginfo_t 结构体访问 union sigval 中的附加数据。

这段代码通过 sigqueue 向子进程发送 SIGUSR1 信号,并附带一个整数值 42。子进程接收到信号后,在信号处理函数中打印该整数值。

五、信号量

 3个API:

semget、semctl、semop

1、semget 函数用于创建一个新的信号量集或获取一个现有的信号量集。它的函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
参数
  • key:信号量集的键值,用于标识信号量集。可以使用 ftok 函数生成。
  • nsems:信号量集中的信号量数量。如果是获取现有的信号量集,该值可以为 0。
  • semflg:标志参数,用于指定创建方式和访问权限,例如 IPC_CREAT0666(读写权限)。
返回值

成功时返回信号量集的标识符(非负整数),失败时返回 -1,并设置 errno

示例代码

以下是一个简单的示例代码,展示如何使用 semget 函数来创建一个新的信号量集:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>

int main() {
    // 使用 ftok 生成唯一的键值
    key_t key = ftok("semfile", 'A');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 创建一个新的信号量集,包含 1 个信号量
    int semid = semget(key, 1, 0666 | IPC_CREAT);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    printf("Semaphore ID: %d\n", semid);
    return 0;
}

 创建信号量集

int semid = semget(key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
    perror("semget");
    exit(EXIT_FAILURE);
}

使用 semget 函数创建一个新的信号量集,包含 1 个信号量。0666 是信号量集的访问权限(读写权限),IPC_CREAT 标志表示如果信号量集不存在则创建它。如果调用成功,返回信号量集的标识符(非负整数);如果失败,返回 -1,并设置 errno

2、semctl 函数是用于控制信号量集或信号量的操作函数。它可以用于初始化信号量的值、获取信号量的值、删除信号量集等操作。semctl 函数的原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
参数
  • semid:信号量集的标识符,由 semget 返回。
  • semnum:信号量集中的信号量编号(索引)。
  • cmd:控制命令,定义了要执行的操作。
  • 变长参数:取决于 cmd 的类型,可以是一个整数值或一个指向 union semun 的指针。

常用控制命令

  • SETVAL:设置单个信号量的值。
  • GETVAL:获取单个信号量的值。
  • IPC_RMID:删除信号量集。
  • SETALL:设置信号量集中所有信号量的值。
  • GETALL:获取信号量集中所有信号量的值。

union semun

为了使用 semctl 的一些控制命令,需要定义一个 union semun。虽然 POSIX 标准中并没有在头文件中定义该联合体,但实际使用时需要手动定义:

union semun {
    int val;                // 值用于 SETVAL
    struct semid_ds *buf;   // 缓冲区用于 IPC_STAT, IPC_SET
    unsigned short *array;  // 数组用于 GETALL, SETALL
};

示例代码

下面展示如何使用 semctl 来初始化信号量、获取信号量值以及删除信号量集。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>

// 定义 union semun
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int main() {
    // 使用 ftok 生成唯一的键值
    key_t key = ftok("semfile", 'A');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 创建一个新的信号量集,包含 1 个信号量
    int semid = semget(key, 1, 0666 | IPC_CREAT);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    // 初始化信号量
    union semun sem_union;
    sem_union.val = 1; // 设置信号量的初始值
    if (semctl(semid, 0, SETVAL, sem_union) == -1) {
        perror("semctl SETVAL");
        exit(EXIT_FAILURE);
    }

    // 获取信号量的值
    int sem_value = semctl(semid, 0, GETVAL);
    if (sem_value == -1) {
        perror("semctl GETVAL");
        exit(EXIT_FAILURE);
    }
    printf("Semaphore value: %d\n", sem_value);

    // 删除信号量集
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        exit(EXIT_FAILURE);
    }

    // 再次尝试获取信号量集,检查是否删除成功
    semid = semget(key, 1, 0666);
    if (semid == -1) {
        if (errno == ENOENT) {
            printf("Semaphore set successfully deleted.\n");
        } else {
            perror("semget after IPC_RMID");
            exit(EXIT_FAILURE);
        }
    } else {
        printf("Semaphore set still exists with ID: %d\n", semid);
    }

    return 0;
}

3、在 Linux 中,semop 函数是用于对信号量集进行操作的系统调用。它的主要作用是对信号量进行 P(等待)操作和 V(释放)操作,从而实现进程间的同步与互斥。semop 的定义如下:

semop 函数原型 

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

参数说明

  • semid:信号量集的标识符,由 semget 函数返回。
  • sops:指向 struct sembuf 结构数组的指针,该数组描述了要执行的信号量操作。
  • nsopssops 数组中的操作数量。(有几个信号量,就是几)

struct sembuf 结构

struct sembuf 结构定义了对单个信号量的操作:

struct sembuf {
    unsigned short sem_num;  // 信号量集中的信号量编号(索引)
    short sem_op;            // 要执行的操作
    short sem_flg;           // 操作标志
};

sem_op

  • sem_op > 0:释放信号量,增加信号量的值。
  • sem_op < 0:等待信号量,减少信号量的值。如果信号量的值小于操作的绝对值,则进程会阻塞,直到信号量的值足够大。
  • sem_op == 0:等待信号量变为零。

sem_flg 标志

  • IPC_NOWAIT:如果操作无法立即执行,则不阻塞进程,立即返回错误。
  • SEM_UNDO:如果进程在操作信号量后终止,系统会自动撤销操作。

示例代码

下面是一个使用 semop 函数进行 P 操作和 V 操作的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <errno.h>

// 定义 union semun
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

void sem_op(int semid, int sem_num, int sem_op);

int main() {
    // 使用 ftok 生成唯一的键值
    key_t key = ftok("semfile", 'A');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 创建一个新的信号量集,包含 1 个信号量
    int semid = semget(key, 1, 0666 | IPC_CREAT);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    // 初始化信号量
    union semun sem_union;
    sem_union.val = 1; // 设置信号量的初始值
    if (semctl(semid, 0, SETVAL, sem_union) == -1) {
        perror("semctl SETVAL");
        exit(EXIT_FAILURE);
    }

    // Fork 一个子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("Child process trying to decrease semaphore...\n");
        sem_op(semid, 0, -1);  // P 操作
        printf("Child process successfully decreased semaphore. Critical section.\n");
        sleep(2); // 模拟在临界区的工作
        sem_op(semid, 0, 1);   // V 操作
        printf("Child process released semaphore.\n");
    } else {
        // 父进程
        printf("Parent process trying to decrease semaphore...\n");
        sem_op(semid, 0, -1);  // P 操作
        printf("Parent process successfully decreased semaphore. Critical section.\n");
        sleep(2); // 模拟在临界区的工作
        sem_op(semid, 0, 1);   // V 操作
        printf("Parent process released semaphore.\n");

        // 等待子进程结束
        wait(NULL);

        // 删除信号量集
        if (semctl(semid, 0, IPC_RMID) == -1) {
            perror("semctl IPC_RMID");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}

void sem_op(int semid, int sem_num, int sem_op) {
    struct sembuf sops;
    sops.sem_num = sem_num;
    sops.sem_op = sem_op;
    sops.sem_flg = 0;

    if (semop(semid, &sops, 1) == -1) {
        perror("semop");
        exit(EXIT_FAILURE);
    }
}

代码解释

  1. 生成键值和创建信号量集

    key_t key = ftok("semfile", 'A');
    int semid = semget(key, 1, 0666 | IPC_CREAT);
    

    使用 ftok 生成键值,并使用 semget 创建一个信号量集。

  2. 初始化信号量

    union semun sem_union;
    sem_union.val = 1;
    if (semctl(semid, 0, SETVAL, sem_union) == -1) {
        perror("semctl SETVAL");
        exit(EXIT_FAILURE);
    }
    

    使用 semctl 将信号量的初始值设为 1。

  3. 定义 P 操作和 V 操作

    void sem_op(int semid, int sem_num, int sem_op) {
        struct sembuf sops;
        sops.sem_num = sem_num;
        sops.sem_op = sem_op;
        sops.sem_flg = 0;
    
        if (semop(semid, &sops, 1) == -1) {
            perror("semop");
            exit(EXIT_FAILURE);
        }
    }
    

    该函数用于执行 P 操作和 V 操作。参数 sem_op 为负值时表示 P 操作,为正值时表示 V 操作。

  4. 父子进程的同步

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("Child process trying to decrease semaphore...\n");
        sem_op(semid, 0, -1);  // P 操作
        printf("Child process successfully decreased semaphore. Critical section.\n");
        sleep(2); // 模拟在临界区的工作
        sem_op(semid, 0, 1);   // V 操作
        printf("Child process released semaphore.\n");
    } else {
        // 父进程
        printf("Parent process trying to decrease semaphore...\n");
        sem_op(semid, 0, -1);  // P 操作
        printf("Parent process successfully decreased semaphore. Critical section.\n");
        sleep(2); // 模拟在临界区的工作
        sem_op(semid, 0, 1);   // V 操作
        printf("Parent process released semaphore.\n");
    
        // 等待子进程结束
        wait(NULL);
    
        // 删除信号量集
        if (semctl(semid, 0, IPC_RMID) == -1) {
            perror("semctl IPC_RMID");
            exit(EXIT_FAILURE);
        }
    }
    

    通过 fork 创建子进程,然后父子进程分别执行 P 操作进入临界区,执行完临界区任务后执行 V 操作释放信号量。最后,父进程删除信号量集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值