目录
1、前言
2、进程间通信的分类
管道
System V IPC
POSIX IPC
3、管道
3.1 匿名管道
pipe
实例代码
管道读写规则:
管道特点
3.2 命名管道
创建一个命名管道
两个进程间使用命名管道进行通信示例代码
4、system V共享内存
4.1 共享内存数据结构
4.2 共享内存函数
shmget函数
shmat函数
shmdt函数
shmctl函数
实例代码
结语
1、前言
在程序运行期间,每一个程序,每一个进程都是独立的,包括父子进程,写时拷贝也保证了每个进程的独立性,但是呢,在某些场景,就是需要我们进行不同的进程间通信,交互数据,今天咱们就来看看在单机模式下,有哪些可以进行进程间通信的方式呢。
2、进程间通信的分类
管道
匿名管道pipe
命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
3、管道
管道是Unix中最古老的进程间通信的形式。从一个进程连接到另一个进程的一个数据流称为一个“管道”

3.1 匿名管道
pipe
#include <unistd.h>
int pipe(int fd[2]);功能:
创建一无名管道
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:
成功返回0,失败返回错误代码
实例代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <cstring>
using namespace std;
int main() {
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n == 0);
    (void)n;
    int fd = fork();
    if (fd < 0) {
        perror("fork");
        return 1;
    }
    if  (fd == 0) {
        //这是子进程,用来接收数据,关闭用以写入数据的端口
        close(pipefd[1]);
        char buff[1024];
        memset(buff, '\0', sizeof(buff));
        while (read(pipefd[0], buff, sizeof(buff)) > 0) {
            
            cout << "子进程接收到的内容:" << buff << endl;
        }
        close(pipefd[0]);
        exit(1);
    }
    //这是父进程,需要关闭读端端口
    close(pipefd[0]);
    for (int i = 0; i < 10; ++i) {
        write(pipefd[1], "hello ltx!", 10);
        sleep(1);
    }
    close(pipefd[1]);
    return 0;
}运行结果

使用fork来完成进程间通信的原理
fork前:

fork后:

站在文件描述符角度,深度理解:
在fork前,父进程打开了一个文件,这个文件被放在files_struct中,创建子进程之后,子进程会继承父进程的files_struct,也会继承在其中指针指向的文件,这样,父子进程就指向了同一个文件,

 
 
管道读写规则:
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
管道特点
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
3.2 命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件,这类特殊的文件不会保存数据,只作用于内存中,充当缓冲区的作用。
创建一个命名管道
mkfifo filename命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);创建命名管道
int main(int argc, char *argv[]) 
{
    mkfifo("pipe.p", 0666); 
    return 0;
}匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
两个进程间使用命名管道进行通信示例代码
server.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;
int main() {
    if (mkfifo("myfifo", 0666) < 0) {
        perror("mkfifo");
        exit(1);
    }
    int fd = open("myfifo", O_WRONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }
    
    char buff[1024];
    while (1) {
        memset(buff, '\0', sizeof(buff));
        fflush(stdout);
        ssize_t n = read(0, buff, sizeof(buff) - 1);
        if (n > 0){
            buff[n] = 0;
            write(fd, buff, strlen(buff));
        }
        else  {
            perror("read");
            break;
        }
    }
    close(fd);
    return 0;
}user.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
using namespace std;
int main() {
    int fd = open("myfifo", O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }
    char buff[1024];
    while (true) {
        memset(buff, '0', sizeof(buff));
        ssize_t n = read(fd, buff, sizeof(buff) - 1);
        if (n > 0){
            buff[n] = 0;
            cout << "接收到的数据:" << buff;
        }
        else if (n == 0) {
            cout << "接收完毕" << endl;
            break;
        }else {
            perror("read");
            break;
        }
    }
    close(fd);
    return 0;
}运行结果


4、system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
内存共享示意图(仅为个人理解,如有错误,请私信作者):

4.1 共享内存数据结构
struct shmid_ds {
    struct ipc_perm     shm_perm;   /* operation perms */
    int         shm_segsz;  /* size of segment (bytes) */
    __kernel_time_t     shm_atime;  /* last attach time */
    __kernel_time_t     shm_dtime;  /* last detach time */
    __kernel_time_t     shm_ctime;  /* last change time */
    __kernel_ipc_pid_t  shm_cpid;   /* pid of creator */
    __kernel_ipc_pid_t  shm_lpid;   /* pid of last operator */ 
    unsigned short      shm_nattch; /* no. of current attaches */ 
    unsigned short      shm_unused; /* compatibility */
    void            *shm_unused2;   /* ditto - used by DIPC */ 
    void            *shm_unused3;   /* unused */
};Linux命令行中可以使用命令来查看共享内存
ipcs -m #查看共享内存
ipcrm -m #根据shmid释放共享内存4.2 共享内存函数
shmget函数
int shmget(key_t key, size_t size, int shmflg);功能:用来创建共享内存
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmat函数
void *shmat(int shmid, const void *shmaddr, int shmflg);功能:将共享内存段连接到进程地址空间
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
注意:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
 int shmdt(const void *shmaddr);功能:将共享内存段与当前进程脱离
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);功能:用于控制共享内存
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

实例代码
comm.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstdio>
#define PATHNAME "/home/ltx"
#define PROJ_ID 100 
#define SIZE 4096
using namespace std;
static int commshm(int size, int flags){
    key_t _key = ftok(PATHNAME, PROJ_ID);
    if (_key < 0) {
        perror("ftok");
        exit(1);
    }
    int shmid = shmget(_key, size, flags);
    if (shmid < 0) {
        perror("shmget");
        exit(2);
    }
    cout << "共享内存创建/获取成功!" << endl;
    return shmid;
}
int shm_create(){
    return commshm(SIZE, IPC_CREAT | IPC_EXCL | 0666);
}
int shm_get(){
    return commshm(SIZE, IPC_CREAT);
}
int shm_destroy(int shmid){
    if (shmid < 0) {
        return -1;
    }
    return shmctl(shmid,IPC_RMID,nullptr);
}server.cpp
#include "comm.hpp"
int main() {
    //创建共享内存
    int shmid = shm_create();
    
    //将共享内存挂到进程中
    char* addr = (char*)shmat(shmid, NULL, 0);
    //cout << addr << endl;
    sleep(2);
    
    //char a = 'a';
    //addr[0] = '\0';
    int i = 26;
    while (i--) {
        cout << "循环读取:";
        printf("client# %s\n", addr);
        sleep(1);
    }
    cout << "读取完毕" << endl;
    shmdt((void*)addr);
    sleep(2);
    int n = shm_destroy(shmid);
    if (n < 0) {
        perror("shm_destroy");
        exit(1);
    }
    
    return 0;
}client.cpp
#include "comm.hpp"
int main() {
    //创建共享内存
    int shmid = shm_get();
    //将共享内存挂到进程中
    char* addr = (char*)shmat(shmid, nullptr, 0);
    sleep(2);
    char a = 'a';
    while (a <= 'z') {
        addr[a - 'a'] = a;
        addr[a - 'a' + 1] = '\0';
        a++;
        sleep(1);
    }
    
    shmdt((void*)addr);
    sleep(2);
    return 0;
}运行结果:

结语
目前先介绍这些,有机会以后再分享,如果有错误,那就请私信作者指出,谢谢!




![java八股文面试[数据库]——MySQL索引的数据结构](https://img-blog.csdnimg.cn/46dccb866952400abe51c0c9661e8bbb.png)














