Linux之进程间通信

x33g5p2x  于2022-03-31 转载在 Linux  
字(14.6k)|赞(0)|评价(0)|浏览(632)

进程间通信

进程间通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

通信的本质是什么?

传递数据,是互相的

为何是进程间通信?进程间能"直接"相互传递数据吗?

这是不可能做到的!因为进程具有独立性,所有的数据操作都会发生写时拷贝,一定要通过中间媒介的方式来进行通信,进程间通信的本质:让不同的进程先看到同一份资源

这个资源是由操作系统提供的,其实就是内存,可以让不同的进程都看到

进程间通信,如何通过系统,让不同的进程看到同一份资源?

进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

进程间通信的本质让不同的进程能看到同一份系统资源(系统通过某种方式提供的系统内存),正是方式不同,因此出现了不同的进程间的通信方式

管道

什么是管道

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

匿名管道

匿名管道的特征
供具有血缘关系的进程,进行进程间通信。(常见于父子)

进程间通信首要任务是保证不同的进程看到同一份资源,父进程以读和写打开两次文件pipe_file,这就是创建管道,然后父进程fork子进程,子进程以父进程为模板进行创建,文件描述符表父子进程不共享,但是值是一样的,此时不同的进程就看到了同一份资源

这就使两个不同的进程看到了相同的内存空间

但是管道只能进行单向数据通信,父子进程需要关闭不需要的文件描述符,来达到构建单向通信的信道的目的,那么有这样几个问题:

  1. 为什么曾经要打开呢?
    不打开读写,子进程拿到的文件打开方式必定和父进程一样,无法通信,打开为了使通信更加灵活

  2. 为何一定要关闭呢?
    防止误操作

pipe函数

头文件:#include <unistd.h>
功能:创建一无名管道
原型:int pipe(int fd[2]);
参数fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

fd是输出型参数,拿到打开的管道文件的描述符:

#include<stdio.h>
#include<unistd.h>
int main()
{
    int pipe_fd[2] = {0};
    if(pipe(pipe_fd) < 0)
    {
        perror("pipe");
        return 1;
    }
    printf("%d,%d\n",pipe_fd[0],pipe_fd[1]);
    return 0;
}

可以看到输出了3和4,3是管道的读端,4是管道的写端

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    //创建管道
    int pipe_fd[2] = {0};
    if(pipe(pipe_fd) < 0)
    {
        perror("pipe");
        return 1;
    }
    printf("%d,%d\n",pipe_fd[0],pipe_fd[1]);
    //fork创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0) // write
    {
        //child
        //关闭读端
        close(pipe_fd[0]);
        const char* msg = "hello parent\n";
        int count = 5;
        while(count)
        {
            write(pipe_fd[1],msg,strlen(msg));//写数据到写端
            sleep(1);
            count--;
        }
        close(pipe_fd[1]);
        exit(0);
    }
    else//read
    {
        char buffer[64];
        //father
        //关闭写端
        close(pipe_fd[1]);
        while(1)
        {
            buffer[0] = 0;
            ssize_t size =  read(pipe_fd[0],buffer,sizeof(buffer)-1);//以读的方式打开管道文件,返回读的个数
            if(size>0)
            {
                buffer[size] = 0;
                printf("parent get message from child:%s\n",buffer);
            }
            else if(size == 0)
            {
                printf("pipe file close,child quit\n");
                break;
            }
            else
            {
                break;
            }
        }
        int status = 0;
        if(waitpid(id,&status,0))
        {
            printf("child quit,wait success\n");

        }
    }
    return 0;
}

可以看到文件描述符为3和4,父子进程成功的交换了数据

用fork来共享管道原理

fork之后:

fork之后关闭掉各自不需要的文件描述符:

站在文件描述符角度理解管道

  1. 父进程创建管道

  1. 父进程fork出子进程

  1. 父进程关闭写端文件描述符,子进程关闭读端文件描述符

管道读写规则

  • 当没有数据可读时
    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将不再保证写入的原子性。

管道的特性

  • 管道自带同步机制

如果管道里面没有消息,父进程(读端),在干什么呢?
父进程在进行等待,等管道内部有数据就绪

如果管道里面写端已经写满了,继续写入,还能写吗?

不能,子进程在等待,等待管道内部有空闲空间

这就叫进程间同步

  • 匿名管道是单向通信的

  • 匿名管道是面向字节流的
    管道是面向字节流的(发送方发送的方式和接受方接收方式不要求一样,比如A一次发50个字节,可B可以一次接收5个,接收10次)

  • 匿名管道只能够保证具有血缘关系的进程通信,常用于父子

  • 匿名管道可以保证一定程度的数据原子性

进程退出,曾经打开的文件也会被关掉,管道也是文件,管道的生命周期随进程的生命周期

下面我们验证部分特征

如果读端不读,并且关闭读端,那么写端如何呢?

管道一般是多大?

read端write端导致结果
不读write阻塞
不写read阻塞
不读并且关闭write端被OS发送SIGPIPE信号杀掉
不写并且关闭read读取到0,文件结束

如果读取关闭,一直写,有意义吗?毫无意义!一直在写,本质就是在浪费系统资源,写进程会立马被操作系统终止掉

写进程是子进程,操作系统通过发送信号的方式干掉子进程,父进程可以通过waitpid获得子进程的状态,下面我们通过代码来验证:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
    int pipe_fd[2] = {0};
    if(pipe(pipe_fd) < 0)
    {
        perror("pipe");
        return 1;
    }
    printf("%d,%d\n",pipe_fd[0],pipe_fd[1]);
    
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 2;
    }
    else if(id == 0) //write
    {
        //child
        //fd[0]:读
        //fd[1]:写
        close(pipe_fd[0]);
        const char*msg = "hello parent\n";
        int count = 5;
        while(count)
        {
            write(pipe_fd[1],msg,strlen(msg));
            sleep(1);
            count--;
        }
        close(pipe_fd[1]);
        exit(0);
    }
    else			//read
    {
        //parent
        close(pipe_fd[1]);
        char buffer[64];
        while(1)
        {
            buffer[0] = 0;
            ssize_t size = read(pipe_fd[0],sizeof((buffer)-1);
            if(size> 0)
            {
                buffer[size] = 0;
                printf("parent get messge from child :%s\n",buffer);
            }   
            else if(size == 0)
            {
                printf("pipe file close,child quit!\n");
                break;
            }
            else
            {
                break;
            }
            close(pipe_fd[0]);//关闭读端
            break;
        }
        int status = 0;
        if(waitpid(id,&status,0) > 0)
        {
            printf("child quit,wait success!,sig:%d\n",status&0x7f);
        }
    }
    return 0;
}

可以看到终止信号为13,13信号是SIGPIPE:

写端被操作系统发送SIGPIPE杀掉

不超过4kb写入都是原子性的,最大为

可以看到是65536个字节,除以1024等于64,64KB

但是这里是512乘以8字节,是4KB

哪个是正确的呢?

man 7 pipe,PIPE_BUF在linux当中是4096字节

Writes of more than PIPE_BUF bytes may be nonatomic,我们可以看到这句话,写入的字节数大于PIPE_BUF时可能是非原子的,这就是为什么用ulimit查看系统资源为什么是4096字节,管道写入不超过4KB,写入都是原子性的

命名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件

1、理解命名管道的原理

要让两个毫不相关的进程通信必须先保证两个进程能看到同一份资源,磁盘上的文件每一个都有路径,路径具有唯一性,可以让不同的进程分别以读和写打开一份文件,让不同的进程进行通信

普通文件是需要将数据刷新到磁盘的,持久化存储的,写入的时候需要将数据刷新到磁盘,读取的时候需要在磁盘上读,但是这样将数据存储在磁盘上效率太低了,实际上系统中上有fifo这样的特殊文件,这个文件有路径来标识自己,但是他不会将数据刷新到磁盘的,这就叫管道文件,当你想要使用命名管道时,普通文件不可以作为通信的媒介,必须是创建的管道文件

2、命令行实验

创建命名管道文件

mkfifo myfifo

可以看到它是一个管道文件

将数据重定向到myfifo文件当中:

while :; do echo "hello world"; sleep 1; done > myfifo

我们重新打开一个bash进程进行查看:

发现向myfifo当中写,文件大小是0,说明就和我们上面所说的不会将数据刷新到磁盘,我们在右边进程中将myfifo重定向到cat,可以看到显示到了该进程当中。此时就通过myfifo管道使两个进程进行了通信:

我们来实现一个客户端与服务端的通信:

我们touch两个文件:client.c和server.c,并且编写Makefile,一次生成两个可执行程序:

.PHONY:all
all:client server
client:client.c
    gcc -o $@ $^
server:server.c
    gcc -o $@ $^
.PHONY:clean
clean:
	rm -f client server

server.c(服务端)

创建命名管道函数:

第一个参数是要制作命名管道的路径,第二个是命名管道的权限

返回值:

成功返回0,失败返回-1

#include<stdio.h>
#include<syd/types.h>
#include<sys/stat.h>
#include<unistd.h>

#define FIFO "./fifo"
int main()
{
    int ret = mkfifo(FIFO,0644);//创建命名管道
    if(ret < 0)
    {
        //创建失败
        perror("mkfifo");
        return 1;
    }
    int fd = open(FIFO,O_RDONLY);//以只读方式打开管道文件
    if(fd < 0)
    {
        perror("open");
        return 2;
    }
    
   	char buffer[128];
    while(1)
    {
        buffer[0] = 0;
        ssize_t s = read(fd,buffer, sizeof(buffer)-1);//读到buffer
        if(s > 0)
        {
            buffer[s] = 0;
            printf( "client# %s\n", buffer);
        }
        else if(s == 0)
        {
        	printf ( "client quit. . ." );
        	break;
        }
        else
        {
            break;
        }
    }
    close(fd);
    return 0;
}

client.c(客户端)

#include<stdio.h>
#include<syd/types.h>
#include<sys/stat.h>
#include<unistd.h>

#define FIFO "./fifo"
int main()
{
    int fd = open(FIFO,O_RDONLY);//以只读方式打开管道文件
    if(fd < 0)
    {
        perror("open");
        return 2;
    }
    
   	char buffer[128];
    while(1)
    {
        printf("Please Enter# ");
        ffulush(stdout);
        buffer[0] = 0;
        ssize_t s = read(0,buffer, sizeof(buffer)-1);//stdin内容读到buffer
        if(s > 0)
        {
            buffer[s] = 0;
            write(fd,buffer,strlen(buffer));
        }
        else if(s == 0)
        {
          	break;
        }
        else
        {
            break;
        }
    }
    close(fd);
    return 0;
}

可以看到在client端发送的消息,server端能够收到

system V共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存的基本原理

经过前面的学习我们知道了进程间通信的本质:
1.要先让不同的进程看到同一份资源

2.通信的过程

前面我们学了管道的通信方式,管道分为匿名管道和命名管道:

匿名管道的本质是利用了父子共享文件的特征

命名管道的本质是利用了文件路径具有唯一性,让进程看到同一个文件

上面的两种管道类通信方式看到的同一份资源都是文件资源

下面我们来看一下系统V的共享内存原理:

  1. 操作系统申请一块内存空间
  2. OS将该内存映射进对应进程的共享区中(堆栈之间)
  3. OS可以将映射后的虚拟地址返回给用户

有一个问题:如果操作系统愿意,能不能做到这些步骤呢?

OS当然能够做到,因为OS是内存管理者以及进程管理者,操作系统内部提供了通信机制(IPC),IPC模块

OS内可不可能提供大量的共享内存呢?
当然是有可能的,共享内存多了OS就要管理:先描述再组织,存在大量的数据结构来描述共享内存,对共享内存的管理就相当于是对数据结构的管理

OS在物理内存申请一块空间叫共享内存,将共享内存映射到进程1,再映射进进程2

  1. 申请共享内存
  2. 进程1和进程2分别挂接对应的共享内存到自己的地址空间(共享区)
  3. 双方就看到了同一份资源,可以进行正常通信了

这三个步骤有对应的系统调用接口,提供类似的服务

申请共享内存接口:shmget

int shmget(key_t key,size_t size,int shmflg)

第三个参数

IPC_CREAT:单独使用时表示目标共享内存不存在,创建之,有,获取之

IPC_EXCL:单独使用没有意义

两个一起使用:如果目标共享内存不存在,创建,如果存在,则出错返回

我们查文档发现当IPC_CREAT和IPC_EXCL一起使用时,如果目标共享内存存在了,则会出错,报出存在的错误信息

如果调用shmget成功,一定得到的是全新的共享内存

那么怎么保证多个进程看到的是同一个共享内存呢?

在创建好共享内存时,操作系统还会创建对应的结构体用来描述该共享内存

struct shm_ipc
{
	//...
}

每一个共享内存都有数据结构,数据结构里面有一个key值

可以通过Key来进行唯一性区分,两个进程在创建共享内存时传入同一个key,此时两个进程就看到了同一份数据结构,那么如何保证AB进程获得的key值一样呢?

此时就又用到了一个系统接口:ftok函数:

ftok接口(创建key值)

两个参数任意填写,但是必须保证AB进程填的是一样的,这样就可以保证这两个进程使用的同一个规则形成key,在底层会通过某种规则对这两个参数进行操作形成一个key值

创建key值:

#include"comm.h"
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
int main()
{
    //创建key值
    key_t k =ftok(PATH_NAME,PROJ_ID);//形成key值
    if(k<0)
    {
        perror("ftok");
        return 1;
    }
    printf("my key:%x\n",k);
    return 0;
}

shmat接口(将当前进程和共享内存关联)

参数:

  • 第一个参数shmid,共享内存标识
  • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
  • 第三个参数shmflg,表示关联共享内存时设置的某些属性。

返回值:

  • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
  • shmat调用失败,返回(void)-1。*

shmdt(将当前进程和共享内存去关联)

参数:

  • 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

返回值:

  • shmdt调用成功,返回0。
  • shmdt调用失败,返回-1。

shmctl(删除共享内存接口)

参数:

  • 第一个参数shmid,表示由shmget返回的共享内存标识码。
  • 第二个参数cmd,表示将要采取的动作(有三个可取值)。
  • 第三个参数buf,是指向一个保存着共享内存的模式状态和访问权限的数据结构。

shmctl函数的第二个参数传入的常用的选项有以下三个:

命令解释
IPC_STAT将shmid_ds结构中的设置为共享内存的当前关联值
IPC_SET在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
IPC_RMID删除共享内存段

了解了以上函数,我们就可以完成共享内存的创建到释放整个流程:

#include"comm.h"
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
int main()
{
    //创建key值
    key_t k =ftok(PATH_NAME,PROJ_ID);//形成key值
    if(k<0)
    {
        perror("ftok");
        return 1;
    }
    printf("my key:%x\n",k);
    
    //创建共享内存
    int shmid = shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);//如果目标共享内存不存在,创建,如果存在,则出错返回
    if(shmid < 0)
    {
        perror("shmget");
        return 2;
    }
    printf("shmid:%d\n",shmid);
    sleep(5);
    
    //将当前进程和共享内存进行关联
    char* start = (char*)shmat(shmid,NULL,0);
    printf("server already attach on shared memory\n");
    
    //开始使用共享内存通信了
    //...
    //共享内存被映射进了地址空间
    /*for( : : )
    {
        printf("%s\n",start);
        sleep(1);
    }*/
    sleep(2);
    
    //将当前进程和共享内存去关联
    shmdt(start);
    printf("server already dattch off shared memory\n");
    sleep(5);
    
    //释放共享内存
    shmctl(shmid,IPC_RMID,NULL);
    printf("delete shm!\n");
    return 0;
}

我们接下来实现两个进程的通信:

共享内存方式进程通信

Makefile的编写

CC=gcc
.PHONY:all
all:client server
client:client.c
    $(CC) -o $@ $^
server:server.c
   	$(CC) -o $@ $^
.PHONY:clean
clean:
	rm -f client server

comm.h

#pragma once
#include<stdio.h>
#define PATH_NAME " "
#define PROJ_ID 0x6666

#define SIZE 4097

我们在comm.h里面定义形成key的参数以及创建共享内存的参数SIZE

server.c

server.c用来创建共享内存

#include"comm.h"
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
int main()
{
    //创建key值
    key_t k =ftok(PATH_NAME,PROJ_ID);//形成key值
    if(k<0)
    {
        perror("ftok");
        return 1;
    }
    printf("my key:%x\n",k);
    
    //创建共享内存
    int shmid = shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);//如果目标共享内存不存在,创建,如果存在,则出错返回
    if(shmid < 0)
    {
        perror("shmget");
        return 2;
    }
    printf("shmid:%d\n",shmid);
    sleep(5);
    
    //将当前进程和共享内存进行关联
    char* start = (char*)shmat(shmid,NULL,0);
    printf("server already attach on shared memory\n");
    
    //开始使用共享内存通信了
    //...
    //共享内存被映射进了地址空间
    for( : : )
    {
        printf("%s\n",start);
        sleep(1);
    }
    sleep(10);
    
    //将当前进程和共享内存去关联
    shmdt(start);
    printf("server already dattch off shared memory\n");
    sleep(5);
    
    //释放共享内存
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

client.c

client需要和server获取一个key才能看到同一块内存,shmget的第三个参数只写IPC_CREAT即可,单独使用时表示目标共享内存不存在,创建之,有,获取之,这里就不需要创建了,获取它就好了

#include"comm.h"
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
int main()
{
    //获取同一个key
    key_t k =ftok(PATH_NAME,PROJ_ID);//形成key值
    if(k<0)
    {
        perror("ftok");
        return 1;
    }
    //printf("%x\n",k);
    
    //不需要自己创建共享内存,获取共享内存
    int shmid = shmget(k,SIZE,IPC_CREAT);
    if(shmid < 0)
    {
        perror("shmget");
        return 2;
    }
    //挂接自己到shm
    char* start = (char*)shmat(shmid,NULL,0);
   	sleep(10);
    //通信
    char c = 'A';
    while(c<='Z')
    {
        start[c-'A'] = c;//向共享内存段写数据
        c++;
        sleep(2);
    }
    //去关联
    shmdt(start);
    //不需要释放共享内存
    return 0;
}

查看共享内存命令:ipcs -m

我们发现当我们再次运行server时,共享内存会创建失败:

是因为进程虽然已经结束了,但是server创建的ipc资源还是存在的,所有的ipc资源都是随内核的,不随进程,不会因为进程退出,ipc资源被释放,释放方式:

  • 进程退出的时候用相关的调用接口释放之或者指令(ipcrm -m 4,4是shmid)。
  • 操作系统进行重启

我们发现ipcs -m查看共享内存信息时还有一个nattch:

nattch:与当前共享内存关联的进程

我们运行server可以发现nattch多了1,server进程已经关联创建的共享内存了:

总结

  1. 共享内存的生命周期随系统
  2. 共享内存不提供任何同步与互斥的操作,双方彼此独立
  3. 共享内存是所有的进程间通信速度最快的

下面我们说一下共享内存的大小:
我们在一开始写comm.h时,定义申请共享内存的大小是4097,我们在申请共享内存时尽量申请4KB的整数倍,因为系统在分配共享内存的时候是按照4KB为基本单位的,即便申请的是4097,但是分配的是4096+4096,这样会存在共享内存浪费,4095就浪费了

shmid和key

key:是一个用户层生成的唯一键值,核心作用是为了唯一性,不能用来进行IPC资源的操作,类比文件的inode号

shmid:是一个系统给我们返回的IPC资源标识符,用来进行操作IPC资源,类比文件的fd

共享内存的数据结构shmid_ds

OS内可不可能提供大量的共享内存呢?可能,共享内存多了OS就要管理:先描述再组织,存在大量的数据结构来描述共享内存,对共享内存的管理就相当于是对数据结构的管理

system V 共享内存数据结构:

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 */
};

system V系列通信每一个结构体里面都有ipc_perm,它们里面描述的都是一样的东西,并且共享内存数据结构里面还有共享内存的其他的一些信息:比如我们前面讲的nattch,共享内存的链接数

消息队列结构体:

信号量结构体:

我们发现System V系列的结构体里面都有ipc_perm成员,在内核中有一个数组来专门存储struct ipc_perm ,我们每申请一个ipc资源,就在该数组中开辟这样的结构,将ipc_perm成员组织成数组,shmid_ds sd,&sd.shm_perm,然后&sd.shm_perm强转为struct shmid_ds,然后可以访问该ipc资源的每一个成员*

我们可以在这个路径下找ipc_perm结构体:

/usr/include/bits/ipc.h

我们可以看到我们前面讲的key就是在ipc_perm结构体中描述的

system V消息队列

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • 特性方面:
    IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

消息队列基本原理

消息队列的接口和共享内存的类似,这里就不做讲解。

比如获取消息队列:

int msgget(key_t key,int msgflg);

system V信号量

进程通信的本质是让进程看到同一份资源(内存空间),这种资源叫做临界资源,在多执行流的情况下,有可能造成数据不一致的问题!进程内的所有代码,不是所有的代码都在访问临界资源,而是一部分在访问,可能造成数据不一致的,是这部分少量访问临界资源的代码,这些代码叫做临界区代码。为了避免数据不一致,保护临界资源,需要对临界区代码进行某种保护(互斥)

互斥

一部分空间任何时候有且只能有一个进程在进行访问,互斥是串行化的执行(锁,二元信号量)

互斥是如何保证的?

加锁和解锁是有对应的代码的,本质是对临界区进行加锁和解锁,完成互斥操作

原子性是什么?

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着"同生共死"的感觉 。一件事情要么做了要么没做

举个例子:

有一个招商银行和农业银行,招商银行里有你1000元,农业银行有500元,当你要进行转账操作,想要将招商银行的200元转到招商银行,招商账号的钱要进行-= 200元的操作,农业银行账号做+=200元的操作,有某些原因导致招商银行钱减了,但是农业银行没有加,这里就是非原子的,中途的操作被干扰了导致结果出错,这就是非原子的,原子性表示要么全部执行成功要么全部执行失败。

什么叫做信号量(灯)

先举个现实生活中的例子:看电影时我们要买票,我们买完票觉得那个座位就属于我们的,现实生活中,有很多场景是有"预定"机制,通过预定可以保证一人一座,电影院的售票系统卖出去的票不能超过电影院的承受能力

进程在访问共享内存时有这样两个场景

  • 场景一:一些进程想访问共享内存,需要遵守互斥机制
  • 场景二:有多个进程想访问共享内存,可以多放进来几个进程,只不过这几个进程访问共享内存的不同区域,可以并行访问

信号量:本质是一个计数器

信号量本质上是一个计数器int count = 4(错误的,这里方便理解所以这样说,原因后面会说),记录共享内存独立的区域个数,当有一个进程去申请共享内存时,就可以使用预定机制:

if(count <= 0)
{
    pause;
}
else
{
    count--;
}

当一个进程申请资源成功时count–,释放资源时,count++,信号量是用来描述临界资源中资源数目的计数器,一个进程申请资源成功时一定有一个资源给你使用,我们把申请信号量操作成为P操作,归还信号量操作称为V操作

有这样两个问题:

多个进程能不能操作同一个count值呢?
不能,因为进程之间具有独立性,会写时拷贝,所以信号量是不等于count的。

信号量是干什么的?

描述临界资源的数目,保护临界资源的安全性

即使信号量就是全局变量count,那么在分配资源时,需要经过以下几个步骤:

  1. if判断资源够不够
  2. 内存将指令给CPU
  3. CPU进行指令操作
  4. CPU将结果给内存
    分配资源是多条语句构成,有可能多分配资源出去,不是原子性的,每个进程都得先申请信号量,前提是每个人都得先看到信号量,信号量本身就是一个临界资源,要保护其他临界资源,保护其他临界资源先得保证自己的安全,信号量的PV操作,必须是原子的,这样保证了自己的安全,全局变量count的形式不是原子的

PV操作的伪代码实现:

int count = 3;
begin:
Lock();//加锁,保证操作是原子的
if(count <= 0)
{
	goto begin:
}
else
{
	count--;
}
Unlock();//保证操作是原子的

然后进程内部访问临界资源,比如共享内存等,用完临界资源后,然后进行释放,也就是V操作:

V:

Lock();//保证操作是原子的
count++;//释放
Unlock();//保证操作是原子的
  • 信号量,在多进程环境下,如何保证信号量被多个进程看到?
    通过ftok()函数获得key,通过semget函数不同进程以相同的key创建信号量和获取信号量,然后各自和将信号量和进行链接,此时信号量就被多个进程看到

  • 如果信号量的计数器的值是 1,那么信号量的值不是1就是0,这样的信号量称为二元信号量,这就是一种互斥语义

信号量的创建semget

int semget(key_t key, int nsems, int semflg);

系统可以允许你一次创建多个信号量,第二个参数 nsems表面你想要创建多个信号量,第一个参数和第三个参数与共享内存的接口函数是一样的意思,这里就不做说明

#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include"comm.h"
#include<sys/sem.h>
int main()
{
    key_t k = ftok(PATH_NAME,PROJ_ID);
    int semid = semget(k,5,IPC_CREAT | IPC_EXCL | 0644);
    return 0;
}

可以看到创建了5个信号量:

信号量操作函数semop

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

函数的参数 semid 为信号量集的标识符;参数 sops 指向进行操作的结构体数组的首地址;参数 nsops 指出将要进行操作的信号的个数

相关文章