【Linux】SystemV IPC

进程间通信

  • 一、SystemV 共享内存
    • 1. 共享内存原理
    • 2. 系统调用接口
      • (1)创建共享内存
      • (2)形成 key
      • (3)测试接口
      • (4)关联进程
      • (5)取消关联
      • (6)释放共享内存
      • (7)测试通信
    • 3. 共享内存的特性
  • 二、SystemV 消息队列(了解)
    • 1. SystemV 消息队列原理
    • 2. 系统调用接口
      • (1)创建消息队列
      • (2)形成 key
      • (3)发送/接收数据
      • (4)释放消息队列
  • 三、IPC在内核中的数据结构设计
  • 四、SystemV 信号量
    • 1. 引入概念
    • 2. 理解信号量
    • 3. 了解系统调用接口
      • (1)申请信号量
      • (2)释放信号量
      • (3)操作信号量

一、SystemV 共享内存

1. 共享内存原理

那么我们知道,进程间通信的本质就是先让不同的进程看到同一份资源。我们以前学的管道都是基于文件的,那么我们还有其它方案进行进程间通信吗?有的,那么我们下面学习的共享内存就是由操作系统帮我们在地址空间中进行通信。

我们知道,每一个进程都有自己的 task_struct,也就是有自己的地址空间,然后通过让操作系统在物理内存创建一块内存空间,因为是操作系统,所以它也有资格修改进程的页表、地址空间等。然后将这块内存空间映射到对应进程地址空间的共享区中,最后给应用层返回这个起始的虚拟地址,如下图:

在这里插入图片描述

如上过程,就可以让不同的进程,看到了同一份资源!这个原理就叫做共享内存

所以上面的步骤我们可以分为:

  1. 申请物理内存
  2. 将内存挂接(关联)到进程地址空间
  3. 返回起始地址

如何需要释放共享内存呢?首先需要将进程和共享内存去关联,再去释放共享内存。那么上面的操作,都不是进程直接做的,因为如果是进程去申请空间,那么这个空间就属于这个进程了!就不是共享内存了!所以这些操作都是由操作系统来做的!所以操作系统就必须需要给我们提供一系列的系统调用!

那么系统中肯定不止一两个进程进行进程通信,也就是物理内存中也不止一个共享内存,必定会有很多份,那么操作系统就要管理所有的共享内存!那么操作系统就要对这些共享内存先描述,再组织!所以内核中就得有一个 struct 结构体描述我们申请的共享内存有多大、有多少进程关联等等属性。

2. 系统调用接口

(1)创建共享内存

首先不管怎样,我们得在系统里创建一个共享内存,在 Linux 中创建一个共享内存的系统接口为:shmget(),手册如下:

在这里插入图片描述

在这里插入图片描述

其中返回值,成功返回共享内存的标识符,是一个整数;否则返回 -1,错误码被设置。

  1. size

shmget() 中有三个参数,我们先看第二个参数 size,这个 size 就是需要创建共享内存的大小,单位是字节。

  1. shmflg

关于第三个参数 shmflg,我们先理解,有进程申请空间,就会有进程使用,那么创建共享内存只需要创建一次就够了,其它进程想在这个共享内存中通信的时候,不需要创建了,只需要获取这个共享内存就行了。所以在使用共享内存时,肯定需要通过某种方式去表示如何创建、如何获取这样的概念,那么 shmflg 就是可以表示这些内容,其中有如下选项:

在这里插入图片描述

以上两个选项我们一看就知道,我们以前在学文件的时候也接触过,它们就是宏,而且它们每一个比特位都是不重叠的,用来传标记给系统调用。

其中 IPC_CREAT 表示创建一个共享内存,如果不存在就直接创建,存在就直接获取并返回。如果这个选项单独使用就是以上效果。

IPC_CREAT | IPC_EXCL 表示创建一个共享内存,如果不存在就直接创建,存在就出错返回。那么这两个选项组合使用,就能确保我们申请的共享内存一定是一个新的!

IPC_EXCL 不单独使用。

  1. key

那么问题又来了,系统怎么知道这个共享内存是否存在呢?怎么保证让不同的进程看到同一个共享内存呢?所以这时候就要介绍第一个参数 key 了,就是通过这个参数 key 保证的!

关于参数 key,我们先理解,无论是创建共享内存还是获取共享内存,我们必须要拿到同一个 key,因为拿到同一个 key 才能保证访问的是同一个共享内存!所以 key 是一个数字,它是多少不重要,关键在于它必须在内核中具有唯一性,才能够保证让不同进程进行唯一性标识。

正因为有了 key,第一个进程就可以通过 key 创建共享内存,第二个进程之后,只要拿着同一个 key 就可以和第一个进程看到同一个共享内存了!

那么对于一个已经创建好的共享内存的 key 在哪呢?毫无疑问,key 在共享内存的描述对象中!

那么第一次创建的时候,这个 key 怎么有呢?首先我们需要确保这个 key 具有唯一性,而我们知道,路径天然就具有唯一性,所以我们就可以根据路径这样具有唯一性的属性形成对应的 key,那么在系统中有一个接口可以帮助我们形成一个 key,下面介绍。

(2)形成 key

其中手册如下:

在这里插入图片描述

在这里插入图片描述

返回值就是 key;第一个参数就是路径,第二参数是项目id;这两个参数我们都可以随意传,只要保证可以创建出具有唯一性的 key 即可,如果创建失败,我们只需要修改这两个参数即可。失败的原因可能有系统内存不足。或者 key 的唯一性不足等等。

其实 ftok() 就是一套算法,它会把我们的路径和项目 id 进行了数值的计算,转化为一个数字。

那么这个 key 为什么要我们用户自己形成呢?因为如果是操作系统帮我们形成,我们就无法将这个 key 交给另一个和我们通信的进程了,它也不知道我们需要和哪一个进程通信,只有我们用户才清楚!所以这个 key 是由用户约定的!

(3)测试接口

接下来我们就可以使用这两个接口进行测试了,我们也引入上一次写的日志函数进来,如下代码:

				#define SHM_SIZE 4096const string pathname = "/home/lmy";const int proj_id = 0x2314;log lg;// 获取 keykey_t GetKey(){key_t k = ftok(pathname.c_str(), proj_id);if(k < 0){lg(Fatal, "ftok error: %s", strerror(errno));exit(1);}lg(Info, "get ftok success, key is: %d", k);return k;}// 创建共享内存int GetShareMem(){key_t k = GetKey();int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL);if(shmid < 0){lg(Fatal, "create shmget error: %s", strerror(errno));exit(2);}lg(Info, "get shareMem success, shmid: %d", shmid);return shmid;}

我们启动一个进程A进行测试:

				int main(){int shmid = GetShareMem();sleep(10);lg(Debug, "processA quit!");return 0;}

结果如下:

在这里插入图片描述

我们看到,返回的 keyshmid,它们为什么要同时存在呢?因为 key 是在操作系统内标定唯一性的;而 shmid 只在进程内用来标识资源的唯一性的!

为了方便观察我们可以将 key 打印成十六进制的;当我们创建好共享内存后,再去创建会如何呢?如下:

在这里插入图片描述

如上图,明明我们上次运行的进程A已经结束了,为什么重新创建会失败呢?首先我们可以使用 ipcs -m 查看操作系统内所有的 IPC 资源,如下:

在这里插入图片描述

其中 perms 是权限,我们还没有设置;nattch 表示当前这个共享内存有几个进程和它是关联的。

但是,我们的进程已经退出了,IPC资源还是存在的!这说明共享内存的生命周期是随内核的!如果用户不主动关闭,共享内存会一直存在,除非内核重启或者用户主动关闭。

那么我们可以使用指令 ipcrm -m shmid 直接删除,如下:

在这里插入图片描述

接下来我们就要把权限设置上,那么权限是在 shmget() 的接口第三个参数中设置,如下:

				int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);

我们重新运行观察结果,发现权限就有了:

在这里插入图片描述

当前有进程创建共享内存了,但是也要有进程获取到共享内存,所以接下来我们需要将接口修改一下,让其它进程也可以获取到共享内存,如下:

				int GetShareMem(int flag){key_t k = GetKey();int shmid = shmget(k, SHM_SIZE, flag);if(shmid < 0){lg(Fatal, "create shmget error: %s", strerror(errno));exit(2);}lg(Info, "get shareMem success, shmid: %d", shmid);return shmid;}// 创建共享内存int CreateShm(){return GetShareMem(IPC_CREAT | IPC_EXCL | 0666);}// 获取共享内存int GetShm(){return GetShareMem(IPC_CREAT);}

那么下面我们介绍一下共享内存的大小,我们在上面设置的大小是 4096 字节,如果我们将它设置成 4097 呢?我们尝试一下:

在这里插入图片描述

如上,大小被设置成了 4097 字节,但是共享内存的大小一般是 4096 的整数倍,我们上面设置的 4097 实际上操作系统申请的共享内存大小是 4096 * 2,但是供我们使用的只有 4097 字节!

(4)关联进程

我们现在已经有了共享内存,接下来就要进行对共享内存和进程进行挂接了。那么使用到的系统接口是:shmat(),手册如下:

在这里插入图片描述
在这里插入图片描述

其中第一个参数 shmid 就是我们上面一直在说的 shmid,即创建或获取共享内存接口的返回值。第二个参数 shmaddr 就是我们想让当前的共享内存挂接到共享区的哪个位置,但是一般让系统决定挂接到哪里,所以设置为 nullptr 即可,那么最终挂接到的虚拟地址会以返回值的形式返回给我们。第三个参数 shmflg 就是有关挂接的权限,我们按照共享内存默认的权限即可,设置为0即可。

使用如下:

				int main(){int shmid = CreateShm();char* ret = (char*)shmat(shmid, nullptr, 0);lg(Debug, "attach success");sleep(3);return 0;}

挂接成功后:

在这里插入图片描述

进程退出后:

在这里插入图片描述

(5)取消关联

我们上面演示的结果中,都是进程退出后自动关闭关联的,那么我们也可以使用系统接口取消关联,对应接口为:shmdt(),手册如下:

在这里插入图片描述

那么它只有一个参数 shmaddr,这个参数就是 shmat() 的返回值。

那么我们只需要传入起始地址就可以了吗?它怎么知道这个空间有多大呢?那么共享内存实际上被申请的时候,它有自己的管理属性,那么它自己会记录共享内存有多大,共享内存也必须是连续的,所以在进行地址空间映射的时候,从连续空间加上大小,我们就知道它的范围了,我们只需要知道从哪开始就行了。

接下来我们对该接口进行测试:

				int main(){int shmid = CreateShm();char* ret = (char*)shmat(shmid, nullptr, 0);lg(Debug, "attach success");sleep(3);int r = shmdt(ret);if(r < 0){lg(Fatal, "shmdt errot: %s", strerror(errno));exit(3);}lg(Debug, "shmdt success: 0x%x", ret);sleep(3);return 0;}

结果如下:

在这里插入图片描述

(6)释放共享内存

我们从上面知道,共享内存的生命周期是随内核的,所以每次进程退出后 IPC 资源还是存在的,那么我们也可以使用指令直接把它释放,但是我们还有对应的系统接口释放共享内存,其接口为:shmctl(),手册如下:

在这里插入图片描述

那么第一个参数就是共享内存的 id;关于第三个参数,struct shmid_ds 就是类似于内核当中的管理共享内存所对应的 struct 结构体。

在这里插入图片描述

也就是说,它一定能让我们获取到共享内存的属性,那么我们要查看共享内存的属性还是修改还是什么呢?所以就有了第二个参数 cmd,表明我们要做什么操作,那么 cmd 的选项有如下:

在这里插入图片描述

其中我们需要的是 IPC_RMID,它的作用是标记共享内存被删除。我们删除就不关注共享内存的属性了,所以第三个参数设为 nullptr 即可。那么返回值成功也是返回0,失败返回-1.

测试代码如下:

				int main(){int shmid = CreateShm();char* ret = (char*)shmat(shmid, nullptr, 0);lg(Debug, "attach success");sleep(3);int r = shmdt(ret);if(r < 0){lg(Fatal, "shmdt errot: %s", strerror(errno));exit(3);}lg(Debug, "shmdt success: 0x%x", ret);sleep(3);int n = shmctl(shmid, IPC_RMID, nullptr);if(n < 0){lg(Fatal, "shmctl errot: %s", strerror(errno));exit(4);}lg(Debug, "destory shm done, shmaddr: 0x%x", ret);sleep(3);return 0;}

结果如下:

在这里插入图片描述

(7)测试通信

上面操作我们已经把一个共享内存的整个生命周期写完了,下面就可以让两个进程实现通信了。

首先我们测试一下让两个进程看到同一份资源。代码如下:

进程A:

				int main(){int shmid = CreateShm();char* ret = (char*)shmat(shmid, nullptr, 0);lg(Debug, "attach success");sleep(3);shmdt(ret);lg(Debug, "shmdt success: 0x%x", ret);sleep(3);shmctl(shmid, IPC_RMID, nullptr);lg(Debug, "destory shm done, shmaddr: 0x%x", ret);sleep(3);return 0;}

进程B:

				int main(){int shmid = GetShm();char* ret = (char*)shmat(shmid, nullptr, 0);lg(Debug, "attach success");sleep(3);shmdt(ret);lg(Debug, "shmdt success: 0x%x", ret);sleep(3);return 0;}

我们只需要观察共享内存中的 nattch 即可判断这两个进程是否已经看到了同一份资源:

在这里插入图片描述

如上,我们就能让两个进程看到了同一份资源。

接下来就可以进行通信了,那么我们可以把两个进程中的日志和休眠函数都去掉;我们让进程A进行读取,即把共享内存当作字符串;进程B进行写入,代码如下:

进程A:

				int main(){int shmid = CreateShm();char* ret = (char*)shmat(shmid, nullptr, 0);// 开始通信while(true){// 直接访问共享内存cout << "processB say# " << ret << endl;sleep(1);}shmdt(ret);shmctl(shmid, IPC_RMID, nullptr);return 0;}

进程B:

				int main(){int shmid = GetShm();char* ret = (char*)shmat(shmid, nullptr, 0);// 开始通信while(true){cout << "Please Enter# ";fgets(ret, SHM_SIZE, stdin);}shmdt(ret);return 0;}

如下,我们就可以让两个进程进行通信了:

在这里插入图片描述

所以通过上面的演示,我们知道了一旦有了共享内存,挂接到自己的地址空间中,直接就可以把它当成自己的内存空间来用即可,不需要调用系统调用!而一旦有人把数据写入到共享内存,其实我们立马就能看到了,不需要经过系统调用就能看到数据了!

3. 共享内存的特性

  • 首先我们上面演示的都是两个毫无关系的进程,所以共享内存不需要血缘关系;
  • 共享内存没有数据,读端在读的时候会一直往下读,不会阻塞等待,也就是说,共享内存没有同步互斥之类的保护机制;
  • 共享内存是所有的进程间通信中,速度最快的,因为它的拷贝最少;
  • 共享内存内部的数据,由用户自己维护。

二、SystemV 消息队列(了解)

1. SystemV 消息队列原理

所谓的消息队列,也是由操作系统给我们提供一个内存空间,其实我们就是通过系统接口在操作系统里面创建一个消息队列。

那么想要两个进程进行通信,必须让不同的进程看到同一份资源,我们已经知道了这份资源可以是文件缓冲区、内存块,所以这个公共资源的种类的不同,决定了通信方式的不同。

那么消息队列的公共资源是一个队列,它允许不同的进程向内核中发送数据块,假设进程A将数据块入队列,进程B也将数据块入队列,那么进程A就可以从队列中读取到进程B的数据块。那么进程A和进程B怎么区分这些数据块呢?到底是自己的数据块还是对方的数据块?所以,它们必须区分开来,区分方式就是向内核发送的数据块是带类型的!这个类型就是区分是自己的数据块还是对方的数据块!如下图:

在这里插入图片描述

那么操作系统内部肯定不止一个消息队列,会有非常多的进程进行通信,所以操作系统还要管理消息队列,所以需要先描述,再组织!

我们现在介绍的消息队列和上面的学的共享内存都是 SystemV 标准的,那么它们的标准体现在哪里呢?我们对比一下它们的系统接口函数。

2. 系统调用接口

(1)创建消息队列

				int msgget(key_t key, int msgflg);

在这里插入图片描述

其中参数和返回值都是和共享内存类似的!

(2)形成 key

				key_t ftok(const char *pathname, int proj_id);

而形成一个 key 和共享内存是一模一样的!

(3)发送/接收数据

发送数据:

				int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

其中 msqid 为向指定的消息队列发;msgp 为数据块的起始地址;msgsz 为数据块的大小;msgflg 设为0,阻塞式发就可以了;

在这里插入图片描述

接收数据:

				ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

前三个参数和上面的一样;msgtyp 是数据块的类型;最后一个参数也是和上面一样。其中我们可以看一看数据块的缓冲区,里面有数据块的类型和大小:

在这里插入图片描述

(4)释放消息队列

				int msgctl(int msqid, int cmd, struct msqid_ds *buf);

在这里插入图片描述

其中这三个参数也是和共享内存类似的!

另外,我们还可以用指令查看操作系统中的消息队列,例如 ipcs -q,如下:

在这里插入图片描述

kill 掉消息队列的指令为 ipcrm -q msqid.

三、IPC在内核中的数据结构设计

在介绍 IPC 在内核中的数据结构设计前,我们再先认识一个进程间通信的方式,就是信号量,信号量也和上面学的两个进程间通信方式一样,都是 SystemV 标准的,所以它们都有共同的标准。

例如它们都要在操作系统内部被先描述再管理起来,所以它们都有自己的结构体,被管理起来,我们可以看一下它们被描述的结构体,我们会发现它们都会有共同标准的结构体,也就是命名风格为 struct_xxxid_ds,而且第一个字段类型都是一样的,都是 struct ipc_perm xxx_perm,如下:

  • 共享内存

在这里插入图片描述

  • 消息队列

在这里插入图片描述

  • 信号量

在这里插入图片描述

其中系统中的所有 IPC 资源是被整合在操作系统的一个 IPC 模块当中的。

那么我们看到,无论是共享内存、消息队列还是信号量,它们的第一个字段都是一样的,用的都是同一个结构体。由于操作系统以后管理它们,都是管理它们的数据结构,那么它是如何管理这些数据结构的呢?

其实在操作系统中,它是用数组进行管理的!这个数组的名字为 struct ipc_perm* array[];当我们创建共享内存、消息队列、信号量,它们的结构体中的第一个字段都是一样的,所以就将它们的第一个字段填入到该数组中,如下图:

在这里插入图片描述

所以从此往后,操作系统要管理所有的 IPC 资源,先描述,对不同的资源有不同的描述方式;对所有的资源增删查改转化为对该数据进行增删查改!所以当我们访问某一个资源,操作系统就得定位某一个资源,它需要确定一个资源是否唯一,它就拿着我们给的 key 遍历这个数组,通过这个数组找到每一个IPC资源,通过比较它们第一个字段的结构体中的 __key 就能确认它是否已经被创建了;其中每一个 ipc_perm 结构,它都在数组里,所以它的数组下标就是对应的 shmid 或者 msqid 或者 semid.

那如果我们想访问某个资源的其它属性呢?也就是想访问操作系统所管理的结构的其它成员?其实在操作系统内部,当我们尝试访问某种资源的时候,我们知道它的结构体的第一个成员的地址是放入数组中的,比如这个数组中的某一个下标内容是 addr,那么接下来怎么访问其它属性呢?很简单,假设我们要访问的资源是共享内存的,只要进行 ((struct shmid_ds*)addr)->??? 即可!那么操作系统怎么知道它要强转成什么类型的资源呢?其实在操作系统内部能区分指针指向的对象的类型。

其实这种机制就是多态!struct ipc_perm 就是基类,其它被管理的结构体都是子类!也就是操作系统内部采用的是用C语言的方式实现的多态!

四、SystemV 信号量

1. 引入概念

我们在共享内存中,如果当进程A正在写入,写入了一部分,就被进程B读取走了,导致双方发送和接收的数据不完整,这就是数据不一致问题。那么这种问题应该如何解决呢?下面就要引入几个概念了。

  1. 数据不一致

进程A和进程B看到的同一份资源,叫做共享资源,而这份共享资源如果不加以保护,会导致数据不一致问题。

  1. 互斥访问

而我们可以通过加锁的方式加以保护,此时我们就是通过加锁来保证一种工作状态,叫做互斥访问。也就是说,任何时刻,只允许一个执行流访问共享资源,这就叫做互斥。

  1. 临界资源

我们一般把共享的,任何时刻只允许一个执行流访问的资源,称为临界资源,这种临界资源一般都是内存空间。

  1. 临界区

其实我们写的代码中,只有少部分代码在访问临界资源,这少部分访问临界资源的代码叫做临界区

接下来我们解释一个现象,如果我们有多个进程,都往显示器打印,也就是在并发打印,为什么显示器上的消息会出现错乱混乱或者和命令行混在一起呢?因为显示器是文件,我们都往显示器上打印,所以本质上显示器也是共享资源,而这必将会导致数据不一致问题,而我们也没有对显示器资源加以保护,所以这是正常现象。

2. 理解信号量

其实信号量的本质就是一个计数器!它是用来描述临界资源中的资源数量的多少!

例如我们去看电影买票,我们还没有去看,先买票的本质就是对资源的预定机制。而在买票的时候,必定会有一个票数的计数器,每卖一张票,计数器就减1,放映厅的资源就少一个。当票数的计数器减到0,表示资源已经被申请完毕了。

这就可以类比计算机中,临界资源可以被划分为很多很多的临界资源单位,所以当一个执行流来访问临界资源的时候,我们就可以把一个临界资源单位分配给该执行流。这样就可以提高多执行流访问临界资源的并发度,只要保证它们不访问同一个临界资源单位,可以在一定程度上提高效率。

在这里插入图片描述

这种情况下,我们最怕的就是多个执行流访问同一个资源,或者执行流的数量大于临界资源单位的数量。所以为了避免这些情况,我们就需要引入一个计数器,计数器记录临界资源的数量,每当有一个执行流访问一个临界资源单位,计数器就减一。当计数器等于零的时候,表示资源被申请完了。

所以,申请计数器的过程等同于买票的过程;临界资源等同于放映厅;临界资源单位等同于放映厅内的座位;计数器的多少等同于座位的多少。所以,

  1. 当我们申请计数器成功了,就表示我具有访问资源的权限了
  2. 申请了计数器资源,本质就是对资源的预定机制
  3. 计数器可以有效保证进入共享资源的执行流的数量
  4. 所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器,跟看电影的本质一样

所以我们把这个 “计数器” 叫做信号量

我们把值只能为1或0两态的计数器,叫做二元信号量,本质就是一把锁!

所以我们要访问临界资源,先要申请信号量计数器资源,那么信号量计数器本质不也是共享资源吗?所以信号量计数器也要被加以保护!那么计数器本质就是对一个变量做减减操作,比如 cnt--,那么这个操作也是不安全的。因为它在 C/C++ 上是一条语句,但是它编译成汇编语言后,它会变成多条汇编语句,而进程在运行的时候,可以随时被切换,所以可能在某个汇编语句的时候,进程会被切换走,所以是不安全的!这个问题我们后面多线程再说。

所以现在我们只需要知道,申请信号量,本质就是对计数器减减,这个操作我们称为P操作;释放资源,释放信号量本质是对计数器加加,这个操作称为V操作;所以申请和释放称为PV操作,这种PV操作必须是原子的,也就是说要么就完成,要么就不完成,没有正在完成的概念。

所以总结一下,信号量本质是一把计数器,来进行PV操作,而这个操作是原子的。执行流申请资源,必须先申请信号量的资源,得到信号量之后,才能访问临界资源!信号量值为1、0两态的称为二元信号量,就是互斥功能;申请信号量的本质就是对临界资源的预定机制!

3. 了解系统调用接口

(1)申请信号量

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

在这里插入图片描述

其中 nsems 是申请信号量的数量,但是多个信号量不等于信号量是几,我们一般设为1即可。

(2)释放信号量

				int semctl(int semid, int semnum, int cmd, ...);

在这里插入图片描述

其中可变参数可以不用传。

(3)操作信号量

其中 PV 操作就是根据该函数来完成的:

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

在这里插入图片描述

在这里插入图片描述

系统接口方面现在了解即可,后面多线程我们还会介绍。

最后,那么信号量为什么是进程间通信的一种呢?共享内存和消息队列都可以传数据,所以叫通信,那么信号量没有传数据为什么是进程间通信的一种呢?严格意义上讲,如果把通信定义成数据互相传送数据,那么信号量就不应该是通信的一种,但是通信不仅仅是进行传送数据,互相协同也是!那么要协同,本质也是通信,信号量首先要被所有的通信进程看到!那么信号量本质也是一种共享资源,所以它也算是进程间通信的一种!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2775949.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

H12-821_74

74.在某路由器上查看LSP&#xff0c;看到如下结果&#xff1a; A.发送目标地址为3.3.3.3的数据包时&#xff0c;打上标签1026&#xff0c;然后发送。 B.发送目标地址为4.4.4.4的数据包时&#xff0c;不打标签直接发送。 C.当路由器收到标签为1024的数据包&#xff0c;将把标签…

学习Vue3的第一天

目录 简介 什么是 Vue&#xff1f; 创建Vue3工程 前提条件 基于 vue-cli 创建&#xff08;不推荐&#xff09; 基于 vite 创建&#xff08;推荐&#xff09; 通过 CDN 使用 Vue 代码示例 简介 什么是 Vue&#xff1f; Vue.js 是一个流行的前端 JavaScript 框架&#…

测试管理_利用python连接禅道数据库并自动统计bug数据到钉钉群

测试管理_利用python连接禅道数据库并统计bug数据到钉钉 这篇不多赘述&#xff0c;直接上代码文件。 另文章基础参考博文&#xff1a;参考博文 加以我自己的需求优化而成。 统计的前提 以下代码统计的前提是禅道的提bug流程应规范化 bug未解决不删除bug未关闭不删除 db_…

戴上HUAWEI WATCH GT 4,解锁龙年新玩法

春节将至&#xff0c;华为WATCH GT 4作为一款颜值和实力并存的手表&#xff0c;能为节日增添了不少趣味和便利。无论你是钟情于龙年表盘或定制属于自己的表盘&#xff0c;还是过年用来抢红包或远程操控手机拍全家福等等&#xff0c;它都能成为你的“玩伴”。接下来&#xff0c;…

CentOS 安装 redis 7.2

nginx官网 https://redis.io/download/ 把鼠标放到这里&#xff0c;复制下载地址 在服务器找个文件夹执行命令 wget https://github.com/redis/redis/archive/7.2.4.tar.gz tar -zxvf 7.2.4.tar.gz make make install 看到这几行就说明安装成功了 不放心的话再查看下b…

谷歌 DeepMind 联合斯坦福推出了主从式遥操作双臂机器人系统增强版ALOHA 2

谷歌 DeepMind 联合斯坦福推出了 ALOHA 的增强版本 ——ALOHA 2。与一代相比&#xff0c;ALOHA 2 具有更强的性能、人体工程学设计和稳健性&#xff0c;且成本还不到 20 万元人民币。并且&#xff0c;为了加速大规模双手操作的研究&#xff0c;ALOHA 2 相关的所有硬件设计全部开…

H5 带网站测速引导页源码

H5 带网站测速引导页源码 源码介绍&#xff1a;一款带网站测速功能的引导页源码 下载地址&#xff1a; https://www.changyouzuhao.cn/10717.html

C语言操作符超详细总结

文章目录 1. 操作符的分类2. 二进制和进制转换2.1 2进制转10进制2.1.1 10进制转2进制数字 2.2 2进制转8进制和16进制2.2.1 2进制转8进制2.2.2 2进制转16进制 3. 原码、反码、补码4.移位操作符4.1 左移操作符4.2 右移操作符 5. 位操作符&#xff1a;&、|、^、~6. 逗号表达式…

【DDD】学习笔记-领域实现模型

实现模型与编码质量 领域设计模型体现了类的静态结构与动态协作&#xff0c;领域实现模型则进一步把领域知识与技术实现连接起来&#xff0c;但同时它必须守住二者之间的边界&#xff0c;保证业务与技术彼此隔离。这条边界线应由设计模型明确给出&#xff0c;其中的关键是遵循…

GPT-4模型中的token和Tokenization概念介绍

Token从字面意思上看是游戏代币&#xff0c;用在深度学习中的自然语言处理领域中时&#xff0c;代表着输入文字序列的“代币化”。那么海量语料中的文字序列&#xff0c;就可以转化为海量的代币&#xff0c;用来训练我们的模型。这样我们就能够理解“用于GPT-4训练的token数量大…

初始web服务器(并基于idea来实现无需下载的tomcat)

前言 前面学习了对应的http协议&#xff0c;我们知道了他是在网络层进行数据传输的协议&#xff0c;负责相应数据以及接收数据的规则&#xff0c;但是在人员开发后端的时候不仅仅需要你写io流进行数据传输&#xff0c;还需要你进行对应的tcp协议来进行数据打包发送http协议-CSD…

【MySQL】MySQL表的增删改查(基础)

MySQL表的增删改查&#xff08;基础&#xff09; 1. CRUD2. 新增&#xff08;Create&#xff09;2.1 单行数据全列插入2.2 多行数据 指定列插入 3. 查询&#xff08;Retrieve&#xff09;3.1 全列查询3.2 指定列查询3.3 查询字段为表达式3.4 别名3.5 去重&#xff1a;DISTINCT…

Netty源码系列 之 ChannelPipeline IO处理回顾 源码

目录 ChannelPipeline【包含AbstractUnsafe.write的源码流程&#xff0c;比之前更加深化了&#xff0c;必看】 ChannelPipeline概念回顾 ChannelPipeline的创建 Inbound(输入Handler)所对应的事件传播 Outbound(输出Handler)所对应的事件传播【包含AbstractUnsafe.write的…

一款VMP内存DUMP及IAT修复工具

前言 加壳是恶意软件常用的技巧之一&#xff0c;随着黑客组织技术的不断成熟&#xff0c;越来越多的恶意软件家族都开始使用更高级的加壳方式&#xff0c;以逃避各种安全软件的检测&#xff0c;还有些恶意软件在代码中会使用各种多态变形、加密混淆、反调试、反反分析等技巧&a…

Vue3.0(五):Vue-Router 4.x详解

Vue-Router详解 vue-router教程 认识前端路由 路由实际上是网络工程中的一个术语 在架构一个网络的时候&#xff0c;常用到两个很重要的设备—路由器和交换机路由器实际上就是分配ip地址&#xff0c;并且维护着ip地址与电脑mac地址的映射关系通过映射关系&#xff0c;路由器…

Window环境下使用go编译grpc最新教程

网上的grpc教程都或多或少有些老或者有些问题&#xff0c;导致最后执行生成文件时会报很多错。这里给出个人实践出可执行的编译命令与碰到的报错与解决方法。&#xff08;ps:本文代码按照煎鱼的教程编写&#xff1a;4.2 gRPC Client and Server - 跟煎鱼学 Go (gitbook.io)&…

【MySQL】_JDBC编程

目录 1. JDBC原理 2. 导入JDBC驱动包 3. 编写JDBC代码实现Insert 3.1 创建并初始化一个数据源 3.2 和数据库服务器建立连接 3.3 构造SQL语句 3.4 执行SQL语句 3.5 释放必要的资源 4. JDBC代码的优化 4.1 从控制台输入 4.2 避免SQL注入的SQL语句 5. 编写JDBC代码实现…

《Git 简易速速上手小册》第2章:理解版本控制(2024 最新版)

文章目录 2.1 本地仓库与版本历史2.1.1 基础知识讲解2.1.2 重点案例&#xff1a;回滚错误提交2.1.3 拓展案例 1&#xff1a;利用 git bisect 查找引入 bug 的提交2.1.4 拓展案例 2&#xff1a;合并提交历史 2.2 远程仓库的使用2.2.1 基础知识讲解2.2.2 重点案例&#xff1a;在 …

midnightsun-2018-flitbip:任意地址写

题目下载 启动脚本 启动脚本如下&#xff0c;没开启任何保护 #!/bin/bash qemu-system-x86_64 \-m 128M \-kernel ./bzImage \-initrd ./initrd \-nographic \-monitor /dev/null \-append "nokaslr root/dev/ram rw consolettyS0 oopspanic paneic1 quiet" 2>…

预测模型:MATLAB线性回归

1. 线性回归模型的基本原理 线性回归是统计学中用来预测连续变量之间关系的一种方法。它假设变量之间存在线性关系&#xff0c;可以通过一个或多个自变量&#xff08;预测变量&#xff09;来预测因变量&#xff08;响应变量&#xff09;的值。基本的线性回归模型可以表示为&…