进程间通信方式

1.无名管道/有名管道

2.信号

3.共享队列(system V-IPC)

4.共享内存(system V-IPC)

5.信号量(system V-IPC)

6.套接字


无名管道特征

1.文件没有名字,无法使用open

2.只能用于亲缘进程间

3.半双工工作方式:读写端分开

4.写入操作不具有原子性,会被打断,因此只能用于一对一的简单场景

5.不能使用lseek()来定位

相关API
int pipe(int pipefd[2]);
应用模板
int main(int argc, char *argv[])
{
    int fd[2];
    if (pipe(fd) == -1)
    {
        fprintf(stderr, "errno:%d,%s", errno, strerror(errno));
        exit(-1);
    }
    pid_t x = fork(); // 创建子进程,继承pipe的描述符
    if (x == 0)       // 子进程
    {
        char *s = "I am the child\n";
        write(fd[1], s, strlen(s)); // 1是读,0是写
    }

    if (x > 0) // 父进程
    {
        char buf[30];
        bzero(buf, sizeof(buf));
        read(fd[0], buf, sizeof(buf));
        printf("from the child:%s", buf);
    }
    close(fd[0]); // 因为无名,所以无法用open,但还是需要close
    close(fd[1]);
    return 0;
}


有名管道特征

1.有名字,且存储于普通文件系统中

2.任何有权限的进程都可以使用open函数获取FIFO文件描述符

4.写入操作具有原子性,支持多写者同时进行写操作且数据不会相互践踏。这是与无名管道的最大区别

5.不能使用lseek()来定位

6.FIFO,最先被写入的数据,最先被读出来

相关API
int mkfifo(const char *pathname, mode_t mode);
应用模板
#define FIFO "/tmp/fifo4test"

int main(int argc, char *argv[]) // 发送的进程
{

    if (access(FIFI, F_OK)) // 检查文件是否存在,不存在就创建
    {
        mkfifo(FIFO, 0644);
    }
    int fifo_fd = open(FIFO, O_RDWR);
    char *s = "I am the child\n";
    int n = write(fifo_fd, s, strlen(s)); // 1是读,0是写
    printf("%d bytes have been sended.\n", n);
    close(fifo_fd);
    return 0;
}

int main(int argc, char *argv[]) // 接收的进程
{
    if (access(FIFI, F_OK)) // 检查文件是否存在,不存在就创建
    {
        mkfifo(FIFO, 0644);
    }
    int fifo_fd = open(FIFO, O_RDWR);

    char buf[30];
    bzero(buf, sizeof(buf));

    read(fifo_fd, buf, sizeof(buf));
    printf("from the child:%s", buf);

    close(fifo_fd);
    return 0;
}

信号特征

1.大部分信号都是异步

2.linux信号62个:

1~31是非实时,不可靠信号,响应不排队;

如果目标进程没有及时相应,则随后到达的同样的信号会被丢弃;

每个非实时信号都对应一个系统事件。

当进程的挂起信号中含有实时和非实时信号,则会优先响应实时信号并从大到小依次响应

34~64实时,可靠信号,按接收顺序排队

即使相同的实时信号被同时发送多次也不会被丢弃,而是依次响应

实时信号没有系统事件与之对应

3.对信号的处理:阻塞,被捕捉并响应(按设置的响应函数或忽略),执行默认动作

相关API
int kill(pid_t pid, int sig);                          // 向pid进程发送信号
void (*sighandler_t)(int);                             // 设置的函数,sighandler_t 被定义为指向一个接收单个 int 参数(即信号编号)并返回 void 的函数的指针。这样的函数通常被称为“信号处理函数”或“信号处理程序”。
sighandler_t signal(int signum, sighandler_t handler); // 接收signum信号,并执行handler信号处理函数,一般和kill配套使用
int raise(int sig);                                    // 给自己发送信号
int pause(void);                                       // 挂起等待信号

sigset_t setset;                                  // 创建信号集
int sigemptyset(sigset_t *set);                   // 清空信号集
int sigfillset(sigset_t *set);                    // 将所有信号添加到信号集中
int sigaddset(sigset_t *set, int signum);         // 将特定信号添加到信号集中
int sigdelset(sigset_t *set, int signum);         // 将特定信号从信号集中删除
int sigismember(const sigset_t *set, int signum); // 判断某特定信号是否在信号集中

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 设置阻塞掩码
应用模板
void handler(int sig)
// sig是触发该处理函数的信号值;
// 子进程会不会继承父进程的信号响应函数和阻塞状态,每个进程都有自己的信号屏蔽字(signal mask),用于控制哪些信号在当前是阻塞的(即不会被立即处理)。
// 而且不同的信号能共享一个处理函数
{
    printf("This is a test4signal.sig:%d\n", sig);
}
int main(int argc, char const *argv[])
{
    sigset_t set;                       // 1.创建一个信号集
    sigemptyset(&set);                  // 清空信号集
    sigaddset(&set, SIGINT);            // 2.把需要屏蔽的信号加入到该集合中
    sigprocmask(SIG_BLOCK, &set, NULL); // 3.需要设置该集合的阻塞属性,第一个参数如果是SIG_UNBLOCK,就解除阻塞
    signal(SIGUSR1, handler);
    // 接收信号,第二个参数设置成SIG_IGN是忽略  SIG_DEL是删除
    pause(); // 暂停进程,等待信号。此处可以用while(1),一直等待信号

    return 0;
}

消息队列,共享内存,信号量 (统称为system-V IPC)

1.消息队列,提供带有数据标识的特殊管道

2.共享内存,提供一块物理内存多次映射到不同的进程虚拟空间

3.信号量,对进程或线程的资源进行管理

消息队列相关API
key_t ftok(const char *pathname, int proj_id);                                // 指定路径和未使用过的整数
int msgget(key_t key, int msgflg);                                            // 获取消息队列的ID
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);            // 发送消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 接收消息
int msgctl(int msqid, int cmd, struct msqid_ds *buf);                         // 设置消息队列的属性

应用模板
// 应用模板:
#define PROJ_PATH "."
#define PROJ_ID 10
#define A2B 1L    // 定义消息标识
#define MSGSZ 100 // 定义消息长度

;
struct msgbuf // 定义带标识的消息结构体
{
    long mtype;
    char mtext[MSGSZ]; // 消息最大一般是16384bytes
};

int main()
{
    key_t key = ftok(PROJ_PATH, PROJ_ID);
    int msgid = msgget(key, IPC_CREAT | 0644);
    struct msgbuf buf;
    bzero(&buf, sizeof(buf));
    buf.mtype = A2B;
    strcpy(buf.mtext, "This is a msg test.\n");
    msgsnd(msgid, &buf, strlen(buf.mtext), MSGSZ);
    // 错误处理此处忽略,实际项目需要考虑
    //  ================下面为从消息队列读取消息============//
    struct msgbuf rec;
    bzero(&rec, sizeof(rec));
    msgrcv(msgid, &buf, MSGSZ, A2B, 0); // 0表示等待消息,默认阻塞
    printf("recive the message:%s", rec.mtext);
    //  ================从消息队列读取消息============//
    return 0;
}
共享内存相关API
key_t ftok(const char *pathname, int proj_id);           // 指定路径和未使用过的整数
int shmget(key_t key, size_t size, int shmflg);          // 获取的ID
void *shmat(int shmid, const void *shmaddr, int shmflg); // 对共享内存进行映射
int shmdt(const void *shmaddr);                          // 解除映射
int shmctl(int msqid, int cmd, struct msqid_ds *buf);    // 设置共享内存的属性
信号量相关API
key_t ftok(const char *pathname, int proj_id);           // 指定路径和未使用过的整数
int semget(key_t key, int nsems, int semflg);            // 获取信号量ID
int semop(int semid, struct sembuf *sops, size_t nsops); // 对信号量进行P/V操作
int semctl(int semid, int semnum, int cmd, ...);         // 获取或设置信号量的相关属性

因为systemV IPC 的信号量用的较少,且进程间通信一般可以用POSIX的信号量代替,因此一起简要总结后者。

POSIX的信号量分为有名和无名:有名信号量,是一种特殊的文件,一般会放在系统的特殊文件系统/dev/shm中,不同进程间需要约定一个相同的名字,就能通过这种有名信号量来相互协调;无名信号量,一般用于一个进程内线程间的同步互斥,因为线程共享一个内存空间。

POSIX有名信号量API
sem_t *sem;
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); // 创建/打开一个有名信号量
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 申请资源,即P操作
int sem_post(sem_t *sem);                                          // 释放资源,即V操作
int sem_close(sem_t *sem);                                         // 关闭
int sem_unlink(const char *name);                                  // 删除有名信号量文件

POSIX无名信号量API

sem_t *sem;
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 申请资源,即P操作
int sem_post(sem_t *sem);                                          // 释放资源,即V操作
int sem_destroy(sem_t *sem);
/*
有名信号量和共享内存应用模板
#define PROJ_PATH "."
#define PROJ_ID 10
#define SEMNAME "sem4test" // 定义消息标识
#define SHMSZ 100          // 定义消息长度
int main()
{
    key_t key = ftok(PROJ_PATH, PROJ_ID);              // 指定路径和未使用过的整数
    int shm_id = shmget(key, SHMSZ, IPC_CREAT | 0644); // 获取共享内存的ID
    char *shmaddr = shmat(shm_id, NULL, 0);            // 对共享内存进行映射

    // 创建POSIX有名信号量
    sem_t *s;
    s = sem_open(SEMNAME, O_CREAT, 0644, 0);
    while (1)
    {
        fgets(shmaddr, SHMSZ, stdin);
        sem_post(s); // 向信号量释放资源,即每次进行操作结束后资源量+1
    }
    sem_unlink(SEMNAME);
    //=========================下半部分为接收信号=====================//
    while (1)
    {
        sem_post(s); // 向信号量申请资源,当信息被读完,即信息量为0时被阻塞
        printf("recive message:%s", shmaddr);
    }
    //=========================接收信号=====================//
    sem_close(s);
    return 0;
}

尤其注意,System V的信号量和POSIX的信号量不是一个概念,它们之间存在明显的区别。以下是两者之间的主要区别:

  • 来源与标准:

System V信号量:来源于Unix操作系统的一个分支,即System V版本。

POSIX信号量:来源于“可移植操作系统接口(Portable Operating System Interface)”标准,这是一个由电气与电子工程学会(IEEE)开发,并由ISO(国际标准化组织)和IEC(国际电工委员会)采纳的国际标准。

  • 使用场景:

System V信号量:常用于进程间的同步。

POSIX信号量:常用于线程间的同步,但也可以用于进程间同步,特别是当使用有名信号量时。

  • 实现与复杂性:

System V信号量:使用相对复杂,通常涉及多个步骤和结构体(如struct sembuf)。

POSIX信号量:使用相对简单,通过单一的sem_open调用即可完成信号量的创建、初始化和权限设置。

  • 存储位置:

System V信号量:基于内核,存放在内核空间中。

POSIX信号量:基于内存,信号量值通常存放在共享内存中,有名信号量通过文件系统(如/dev/shm)中的特殊文件来表示。

  • 信号量类型:

System V信号量:通常作为信号量集合存在,每个集合可以包含多个信号量。

POSIX信号量:有两种类型——有名信号量和无名信号量。有名信号量通过IPC名字进行进程间同步,无名信号量则通常用于线程间同步。

  • 信号量操作:

System V信号量:通过semop等系统调用来操作信号量。

POSIX信号量:通过sem_wait(P操作)、sem_post(V操作)等函数来操作信号量。

  • 头文件与接口:

System V信号量:使用<sys/sem.h>头文件,接口函数包括semget、semop等。

POSIX信号量:使用<semaphore.h>头文件,接口函数包括sem_open、sem_wait、sem_post等。


剩下一项套接字,一般用于网络编程,即不同主机间的进程通信,后续补充。