IPC:进程间的通信方式

前言

由于进程的独立性,当我们要使两个进程间进行数据交互的时候就得通过介质来实现。

进程间进行通信的介质我们把它们称之为进程间通信方式(IPC)
在这里插入图片描述
根据进程间通信场景的不同,操作系统为用户提供了几种不同的进程间通信方式:管道、共享内存、消息队列、信号量

管道(数据传输)

管道是一种传输数据的媒介,所谓管道是操作系统在内核创建的一块缓冲区域,进程只要能够访问到这块缓冲区就能通过缓冲区实现相互通信。

同时操作系统会给进程一个操作句柄用于进程操作管道,在Linux下其实是给了进程两个文件描述符,用于负责管道的读和写。

  • 管道的本质:

内核中的一块缓冲区

  • 管道通信的原理:

让多个进程都能访问到同一块缓冲区从而实现进程间通信

  • 管道的特性:

1.管道自带同步与互斥(保证多个进程操作管道不发生数据的二义性)
同步:通过条件判断保证临界资源访问时序的合理性
互斥:通过同一时间的唯一访问保证临界资源访问的安全性(原子操作)

2.管道的生命周期随进程

3.管道实现方向可选择的、单向的、有序的数据传输

4.管道提供字节流服务(有序,连接,可靠的字节流传输)

读写特性:

1.管道中没有数据则读端阻塞,管道中写满了数据则写端阻塞
2.管道读端被关闭继续写入会触发异常发起SIGPIPE信号导致进程退出
3.写端被关闭继续读完管道中的数据后返回0

注意:关闭读端或写端的时候,所有进程的读端或写端都得关闭

匿名管道

匿名管道顾名思义就是内核中的这块缓冲区没有标识符

当前进程只能通过返回的操作句柄来操作匿名管道,其他进程无法找到匿名管道,所以匿名管道只能用于具有亲缘关系的进程间通信。

  • 匿名管道通信流程:

操作句柄保存在pcb当中,先创建管道再创建子进程,子进程通过复制父进程的方式获取管道的操作句柄,进而访问同一管道来实现进程间的通信。

  • 匿名管道的创建:

int pipe(int pipefd[2]):具有两个int型结点的数组的首地址,用于接收创建管道返回的操作句柄

创建一个匿名管道,向用户通过参数pipefd返回管道的操作句柄。返回值 0(成功),-1(失败)

pipefd[0]: 用于从管道读取数据
pipefd[1]: 用于向管道写入数据

  • 匿名管道的实现:
int main(){
    // 匿名管道只能用于具有亲缘关系的进程间通信
    // 通过子进程复制父进程的方式来获取管道的操作句柄
    // 创建管道一定要放到子进程之前
    int pipefd[2] = {-1};
    int ret = pipe(pipefd);
    if(ret < 0){
        perror("pipe error");
        return -1;
    }

    pid_t pid = fork();
    if(pid == 0){
        // 子进程从管道读取数据
        char buf[1024] = {0};
        read(pipefd[0], buf, 1023);
        printf("子进程读到的数据:%s", buf);
    }
    else if(pid > 0){
        // 父进程向管道写入数据
        char* ptr = "父进程写入\n";
        write(pipefd[1], ptr, strlen(ptr));
    }

    return 0;
}
命名管道

命名管道顾名思义就是内核中的这块缓冲区有标识符

  • 命名管道的通信流程:

多个进程可以通过相同的标识符找到同一块缓冲区,打开同一个管道文件,进而实现同一主机上任意进程间通信。

  • 命名管道的创建:

mkfifo 创建命名管道(生成test.fifo的管道文件):int mkfifo(const char* pathname管道文件名称,mode_t mode文件权限)

成功:返回 0 | 失败:返回-1

  • 命名管道的打开特性:

1.若管道文件以只读的方式打开,则会阻塞,直到这个管道文件被以写的方式打开。
2.若管道文件以只写的方式打开,则会阻塞,直到这个管道文件被以读的方式打开。
3.若管道文件以读写的方式打开,则不会阻塞。

  • 命名管道的实现:

Fifowrite.c

int main(){
    // 创建命名管道
    umask(0);
    int ret = mkfifo("./test.fifo", 0664);
    if(ret < 0 && errno != EEXIST){
        perror("mkfifo error");
        return -1;
    }

    // 操作管道
    int fd = open("./test.fifo", O_WRONLY);
    if(fd < 0 ){
        perror("open fifo error");
        return -1;
    }
    printf("open fifo success\n");
    int cur = 0;
    while(1){
        char buf[1024] = {0};
        sprintf(buf, "写端写入数据 [%d]", cur++);
        write(fd, buf, strlen(buf));
        printf("写入数据成功\n");
        sleep(1);
    }
    close(fd);

    return 0;
}

Fiforead.c

int main(){
    // 创建命名管道
    umask(0);
    int ret = mkfifo("./test.fifo", 0664);
    if(ret < 0 && errno != EEXIST){
        perror("mkfifo error");
        return -1;
    }

    // 操作管道
    int fd = open("./test.fifo", O_RDONLY);
    if(fd < 0 ){
        perror("open fifo error");
        return -1;
    }
    printf("open fifo success\n");

    while(1){
        char buf[1024] = {0};
        int ret = read(fd, buf, 1023);
        if(ret < 0){
            perror("read error");
            return -1;
        }
        else if(ret == 0){
            perror("all write closed");
            return -1;
        }
        printf("read buf:[%s]\n", buf);
    }
    close(fd);

    return 0;
}

共享内存(数据共享)

  • 共享内存的本质:

共享内存实际是在物理内存上开辟的一块具有标识符的内存空间

  • 共享内存的通信原理:

将这块内存空间映射到进程的虚拟地址空间中,进程则可以通过虚拟地址进行访问操作;多个进程映射同一块物理内存,那么多个进程访问同一块内存空间,进而实现进程间通信。

  • 共享内存的特性:

1.共享内存生命周期随内核
在物理内存上开辟,将信息储存在内核中,生命不会随进程结束而结束,内核重启或手动释放共享内存

2.最快的进程间通信方式
不涉及用户态和内核态的两次数据拷贝

3.共享内存操作是不安全
不自带同步和互斥,需要操作用户进行控制

4.共享内存的写入是一种针对地址的覆盖式写入

  • 共享内存的通信流程:

Shmwrite.c

#define IPC_KEY 0x12345678

int main(){
    // 1.创建共享内存
    int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
    if(shm_id < 0){
        perror("shmget error");
        return -1;
    }

    // 2.建立映射关系
    void* shm_start = shmat(shm_id, NULL, 0);
    if(shm_start == (void*)-1){
        perror("shmat error");
        return -1;
    }

    // 3.进行内存操作
    int cur = 0;
    while(1){
        sprintf(shm_start, "%s:%d", "写入端写入", ++cur);
        sleep(1);
    }

    // 4.解除映射关系
    shmdt(shm_start);

    // 5.删除共享内存
    shmctl(shm_id, IPC_RMID, NULL);

    return 0;
}

Shmread.c

#define IPC_KEY 0x12345678

int main(){
    // 1.创建共享内存
    int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
    if(shm_id < 0){
        perror("shmget error");
        return -1;
    }

    // 2.建立映射关系
    void* shm_start = shmat(shm_id, NULL, 0);
    if(shm_start == (void*)-1){
        perror("shmat error");
        return -1;
    }

    // 3.进行内存操作
    while(1){
        printf("[%s]\n", shm_start);
        sleep(1);
    }

    // 4.解除映射关系
    shmdt(shm_start);

    // 5.删除共享内存
    shmctl(shm_id, IPC_RMID, NULL);

    return 0;
}

删除共享内存中值得注意:

共享内存并不会立即被删除(因为可能造成正在访问的进程崩溃),而是将key修改为0,表示这块共享内存不再继续接收映射连接,当这块共享内存的映射连接数为0时,则自动被释放。

消息队列(数据传输)

  • 消息队列的本质:

消息队列是内核中的一个优先级队列

  • 消息队列的通信原理:

多个进程通过向队列中添加结点或者获取结点实现通信

  • 消息队列的特性:

1.生命周期随内核
2.自带同步与互斥
3.数据传输自带优先级(传输一个有类型(优先级)的数据块)

信号量(进程控制)

  • 信号量的本质:

内核当中的一个计数器+pcb等待队列(对资源进行计数)

  • 信号量的作用:

用于实现进程间的同步与互斥

同步的实现:

访问前进行条件判断

信号量是对资源的计数,通过计数判断是否能够获取一个资源进行处理,若计数器<0则表示不能获取(并且对计数器-1),需要等待(加入pcb队列),并不保证获取资源的安全性

这时若其他进程生产一个资源,则会对计数进行+1,若计数<=0则唤醒一个进程。 >0社么都不干

将pcb置为可中断休眠状态加入队列

互斥的实现:

同一时间的唯一访问

起始为1

通过不大于1的计数器,实现对临界资源访问状态的标记,在访问临界资源之前先获取信号量

计数 - 1后:

若计数 <= 0则进程等待(将进程pcb加入队列中);否则可以对临界资源进行访问(已经将临界资源的状态置为不可访问状态,完毕之后,对计数+1,唤醒一个进程(将一个pcb出队,置为运行状态)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章