当前位置: 首页 > news >正文

【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);

在这里插入图片描述

在这里插入图片描述

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

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

相关文章:

【Linux】SystemV IPC

进程间通信 一、SystemV 共享内存1. 共享内存原理2. 系统调用接口&#xff08;1&#xff09;创建共享内存&#xff08;2&#xff09;形成 key&#xff08;3&#xff09;测试接口&#xff08;4&#xff09;关联进程&#xff08;5&#xff09;取消关联&#xff08;6&#xff09;释…...

iview 页面中判断溢出才使用Tooltip组件

使用方法 <TextTooltip :content"contentValue"></TextTooltip> 给Tooltip再包装一下 <template><Tooltip transfer :content"content" :theme"theme" :disabled"!showTooltip" :max-width"300" :p…...

如何使用websocket

如何使用websocket 之前看到过一个面试题&#xff1a;吃饭点餐的小程序里&#xff0c;同一桌的用户点餐菜单如何做到的实时同步&#xff1f; 答案就是&#xff1a;使用websocket使数据变动时服务端实时推送消息给其他用户。 最近在我们自己的项目中我也遇到了类似问题&#xf…...

C++ 调用lua 脚本

需求&#xff1a; 使用Qt/C 调用 lua 脚本 扩展原有功能。 步骤&#xff1a; 1&#xff0c;工程中引入 头文件&#xff0c;库文件。lua二进制下载地址&#xff08;Lua Binaries&#xff09; 2&#xff0c; 调用脚本内函数。 这里调用lua 脚本中的process函数&#xff0c;并…...

Centos 内存和硬盘占用情况以及top作用

目录 只查看内存使用情况&#xff1a; 内存使用排序取前5个&#xff1a; 硬盘占用情况 定位占用空间最大目录 top查看cpu及内存使用信息 前言-与正文无关 生活远不止眼前的苦劳与奔波&#xff0c;它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界中&…...

【数据结构】堆(创建,调整,插入,删除,运用)

目录 堆的概念&#xff1a; 堆的性质&#xff1a; 堆的存储方式&#xff1a; 堆的创建 &#xff1a; 堆的调整&#xff1a; 向下调整&#xff1a; 向上调整&#xff1a; 堆的创建&#xff1a; 建堆的时间复杂度&#xff1a; 向下调整&#xff1a; 向上调整&#xff…...

v-if 和v-for的联合规则及示例

第073个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使用&#xff0c;computed&a…...

各互联网企业测绘资质调研

公司子公司产品产品介绍资质获得资质时间阿里巴巴高德高德地图作为阿里的全资子公司&#xff0c;中国领先的数字地图内容、导航和位置服务解决方案提供商&#xff0c;互联网地图行业龙头&#xff0c;2021年4月高德实现全月平均日活跃用户数超过1亿的重要里程碑&#xff0c;稳居…...

C++自定义函数详解

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 铁汁们新年好呀&#xff0c;今天我们来了解自定义函数。 文章目录 1.数学中的函数 2.什么是自定义函数 3.自定义函数如何使用&#xff1f; 4.值传递和引用传递&#xff08;形参和实参区分&#xff09; …...

flask+vue+python跨区通勤人员健康体检预约管理系统

跨区通勤人员健康管理系统设计的目的是为用户提供体检项目等功能。 与其它应用程序相比&#xff0c;跨区通勤人员健康的设计主要面向于跨区通勤人员&#xff0c;旨在为管理员和用户提供一个跨区通勤人员健康管理系统。用户可以通过系统及时查看体检预约等。 跨区通勤人员健康管…...

Spring Boot动态加载Jar包与动态配置技术探究

Spring Boot动态加载Jar包与动态配置技术探究 1. 引言 在当今快节奏的软件开发领域&#xff0c;高效的开发框架是保持竞争力的关键。Spring Boot作为一款快速开发框架&#xff0c;以其简化配置、内嵌Web服务器、强大的开发工具等特性&#xff0c;成为众多开发者的首选。其背后…...

Lua metatable metamethod

示例代码 《programming in lua》里有一个案例很详细&#xff0c;就是写一个集合类的table&#xff0c;其负责筛选出table中不重复的元素并组合成一个新的table。本人按照自己的方式默写了一次&#xff0c;结果发现大差不差&#xff0c;代码如下&#xff1a; Set {} --集合--…...

HCIA-HarmonyOS设备开发认证V2.0-3.2.轻量系统内核基础-任务管理

目录 一、任务管理1.1、任务状态1.2、任务基本概念1.3、任务管理使用说明1.4、任务开发流程1.5、任务管理接口 一、任务管理 从系统角度看&#xff0c;任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源&#xff0c;并独立于其它任务运行。 O…...

中小型网络系统总体规划与设计方法

目录 1.基于网络的信息系统基本结构 2.网络需求调研与系统设计原则 3.网络用户调查 4.网络节点地理位置分布情况 5.网络需求详细分析 6.应用概要分析 7.网络工程设计总体目标与设计原则 8.网络结构与拓扑构型设计方法 9.核心层网络结构设计 10.接入核心路由器 11.汇聚…...

以管理员权限删除某文件夹

到开始菜单中找到—命令提示符—右击以管理员运行 使用&#xff1a;del /f /s /q “文件夹位置” 例&#xff1a;del /f /s /q "C:\Program Files (x86)\my_code\.git"...

JenkinsGitLab完成自动化构建部署

关于GitLab安装:GitLab安装-CSDN博客 Docker中安装GitLab:Docker下安装GitLab-CSDN博客 安装JenKins Jenkins官网:Jenkins 中文版:Jenkins 安装时候中文页面的war包下不来 在英文页面 记得装JDK8以上 JenKins使用java写的 运行JenKins需要JDK环境 我这里已经装好了 将下…...

JVM 性能调优 - 参数基础(2)

查看 JDK 版本 $ java -version java version "1.8.0_151" Java(TM) SE Runtime Environment (build 1.8.0_151-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode) 查看 Java 帮助文档 $ java -help 用法: java [-options] class [args...] …...

大型软件编程实例分享,诊所门诊处方笺管理系统多台电脑同时使用的软件教程

大型软件编程实例分享&#xff0c;诊所门诊处方笺管理系统多台电脑同时使用的软件教程 一、前言 以下教程以 佳易王诊所门诊电子处方管理系统V17.2 为例说明 软件资源可以点击最下方官网卡片了解详情 软件左侧为导航栏 1、系统参数设置&#xff1a;可以设置打印等参数 2、…...

Java基于微信小程序的医院挂号系统

文章目录 1 简介2 技术栈3 系统目标3.2 系统功能需求分析3.2.1 功能需求分析 4 系统模块设计4.1 数据库模块设计 5 系统的实现5.1 微信小程序个人中心5.2 科**室内容查看的实现**5.3 预约挂号的实现5.4 后台管理界面实现5.5 医生预约管理5.6 医生信息管理 参考文献7 推荐阅读8 …...

你是在独立思考,还是在被洗脑?

你有过这样的经历吗&#xff1f; 老板走过来&#xff0c;急匆匆丢给你一句&#xff1a;帮我整理一下那个客户的资料&#xff0c;下午给我。你抬头&#xff0c;应道「好好好」。老板扬长而去。你转念一想&#xff1a; 等等&#xff0c;哪个客户&#xff1f;什么资料&#xff1f;…...

在django中集成markdown文本框

首先需要下载开源组件&#xff1a;http://editor.md.ipandao.com/&#xff0c;可能需要挂梯子。 百度网盘&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1D9o3P8EQDqSqfhAw10kYkw 提取码&#xff1a;eric 1.在html代码中生成一个div&#xff0c;ideditor <div c…...

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Slider组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Slider组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、Slider组件 滑动条组件&#xff0c;通常用于快速调节设置值&#xff0c;如音量调…...

django admin 自定义界面时丢失左侧导航 nav_sidebar

只显示了自定义模板的内容&#xff0c;左侧导航没有显示出来。 原因&#xff1a;context 漏掉了&#xff0c;要补上。 # 错误写法&#xff08;左侧导航不显示&#xff09;def changelist_view(self, request, extra_contextNone):form CsvImportForm()payload {"form&qu…...

JSP原理简述

JSP动态网页技术&#xff0c;可以定义html&#xff0c;css&#xff0c;js等静态内容&#xff0c;还可以定义java代码等动态内容。 注意导入坐标时&#xff0c;JSP的scope标签是provided&#xff0c;和servlet一样&#xff0c;否则会报错。 JSP本质上就是一个Servlet&#xff0c…...

C/C++ - 异常处理

目录 错误处理 异常处理 异常传播 异常规划 标准异常 自定异常 错误处理 在C语言中&#xff0c;错误通常通过函数的返回值来表示。 错误返回值 对于能返回特殊值&#xff08;如NULL或负值&#xff09;的函数&#xff0c;在调用时检查这些值来处理错误。 #include <st…...

十、项目开发总结报告(软件工程)

1&#xff0e;引言 1.1编写目的 1.2项目背景 1.3定义 1.4参考资料 2&#xff0e;开发结果 2.1产品 2.2主要功能及性能 2.3所用工时 2.4所用机时 2.5进度 2.6费用 3&#xff0e;评价 3.1生产率评价 3.2技术方案评价 3.3产品质量评价 4&#xff…...

在 VMware 虚拟机上安装 CentOS系统 完整(全图文)教程

一、前期准备&#xff1a; 1.安装VMware 虚拟机软件&#xff08;不在讲解&#xff0c;可自行去下载安装&#xff09;。官网&#xff1a;https://customerconnect.vmware.com/cn/downloads/details?downloadGroupWKST-PLAYER-1750&productId1377&rPId111471 2.下载iso…...

吉他学习:右手拨弦方法,右手拨弦训练 左手按弦方法

第六课 右手拨弦方法https://m.lizhiweike.com/lecture2/29362775 第七课 右手拨弦训练https://m.lizhiweike.com/lecture2/29362708...

【初识爬虫+requests模块】

爬虫又称网络蜘蛛、网络机器人。本质就是程序模拟人使用浏览器访问网站&#xff0c;并将需要的数据抓取下来。爬虫不仅能够使用在搜索引擎领域&#xff0c;在数据分析、商业领域都得到了大规模的应用。 URL 每一个URL指向一个资源&#xff0c;可以是一个html页面&#xff0c;一…...

微信小程序(三十八)滚动容器

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.滚动触底事件 2.下拉刷新事件 源码&#xff1a; index.wxml <view class"Area"> <!-- scroll-y 垂直滚动refresher-enabled 允许刷新bindrefresherrefresh 绑定刷新作用函数bindscrollto…...

Python学习之路-Tornado基础:数据库

Python学习之路-Tornado基础:数据库 简介 与Django框架相比&#xff0c;Tornado没有自带ORM&#xff0c;对于数据库需要自己去适配。我们使用MySQL数据库。 在Tornado3.0版本以前提供tornado.database模块用来操作MySQL数据库&#xff0c;而从3.0版本开始&#xff0c;此模块…...

Golang的for循环变量和goroutine的陷阱,1.22版本的更新

先来看一段golang 1.22版本之前的for循环的代码 package mainimport "fmt"func main() {done : make(chan bool)values : []string{"chen", "hai", "feng"}for _, v : range values {fmt.Println("start")go func() {fmt.P…...

List 差集

文章目录 基本类型对象类型 基本类型 ListUtils.subtract 方法用于计算两个集合的差集&#xff0c;即返回 list1 中有但 list2 中没有的元素。 其中&#xff0c;list1 指向第一个集合&#xff0c;list2 指向第二个集合。该方法返回一个新的 List 对象&#xff0c;它包含所有在…...

ArcGIS的UTM与高斯-克吕格投影分带要点总结

UTM&#xff08;通用横轴墨卡托投影、等角横轴割椭圆柱投影&#xff09;投影分带投影要点&#xff1a; 1&#xff09;UTM投影采用6度分带 2&#xff09;可根据公式计算&#xff0c;带数&#xff08;经度整数位/6&#xff09;的整数部分31 3&#xff09;北半球地区&#xff0…...

华为第二批难题一:基于预训练AI模型的元件库生成

我的理解&#xff1a;华为的这个难道应该是想通过大模型技术&#xff0c;识别元件手册上的图文内容&#xff0c;与现有建库工具结合&#xff0c;有潜力按标准生成各种库模型。 正好&#xff0c;我们正在研究&#xff0c;利用知识图谱技术快速生成装配模型&#xff0c;其中也涉…...

Android AOSP源码研究之万事开头难----经验教训记录

文章目录 1.概述2.Android源下载1.配置环境变量2.安装curl3.下载repo并授权4.创建一个文件夹保存源码5.设置repo的地址并配置为清华源6.初始化仓库7.指定我们需要下载的源码分支并初始化 2.1 使用移动硬盘存放Android源码的坑2.2 解决方法 3.Android源码编译4.Android源烧录 1.…...

动态数据源

一、部署 1、导入依赖 <dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.1.0</version></dependency>2、编写yml 配置文件 spring:datasource:dyna…...

2024.1.29力扣每日一题——自由之路

2024.1.29 题目来源我的题解方法一 动态规划 题目来源 力扣每日一题&#xff1b;题序&#xff1a;514 我的题解 方法一 动态规划 定义 dp[i][j] 表示从前往后拼写出 key的第 i个字符&#xff0c; ring 的第 j个字符与 12:00 方向对齐的最少步数&#xff08;下标均从 0 开始&…...

Qt应用软件【协议篇】UDP示例

UDP协议简介 UDP(用户数据报协议)是一种无连接的网络协议,提供了简单但是不可靠的消息传输服务。与TCP不同,UDP不保证数据包的顺序、重复性或者可达性,但它在速度和效率上具有优势,特别适合那些对实时性要求高的应用,如视频流、在线游戏等。 Qt中的UDP编程 在Qt中,U…...

MyBatis之动态代理实现增删改查以及MyBatis-config.xml中读取DB信息文件和SQL中JavaBean别名配置

MyBatis之环境搭建以及实现增删改查 前言实现步骤1. 编写MyBatis-config.xml配置文件2. 编写Mapper.xml文件&#xff08;增删改查SQL文&#xff09;3. 定义PeronMapper接口4. 编写测试类1. 执行步骤2. 代码实例3. 运行log 开发环境构造图总结 前言 上一篇文章&#xff0c;我们…...

百面嵌入式专栏(面试题)内存管理相关面试题1.0

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍内存管理相关面试题 。 一、内存管理相关面试题 page数据结构中的_refcount和_mapcount有什么区别?匿名页面和高速缓存页面有什么区别?page数据结构中有一个锁,我们称为页锁,请问trylock_page()和loc…...

SpringMVC 1.请求参数检查 2.全局异常处理 3.请求参数封装为Pojo

ErrorEnum.java // 枚举所有的错误 package com.example.demo.enums;import lombok.Getter;public enum ErrorEnum {SYSTEM_ERROR(-1, "系统错误"),PARAM_ERROR(-2, "参数错误"),OK(0, "成功"),;Getterprivate final int code;Getterprivate fi…...

7机器人位姿的数学描述与坐标变

由上次刚体的空间转动直接切换为机器人相关术语。 1.机器人位姿的数学描述与坐标变换 1.1位姿描述 {B}相对于{A}的姿态描述用3x3矩阵表示为&#xff1a; 式中为三个单位正交主矢量&#xff0c;分别表示刚体坐标系{B}的三个坐标轴XBYBZB在参考系{A}中的方位&#xff0c;∠XBXA表…...

基于ESP8266 开发板(MCU)遥控小车

遥控小车 ​ 遥控界面 ​ 【项目源码】 第一版ESP8266 https://github.com/liyinchigithub/esp8266_car_webServerhttps://github.com/liyinchigithub/esp8266_car_webServer 第二版ESP32 GitHub - liyinchigithub/esp32-wroom-car: 嵌入式单片机 ESP32 Arduino 遥控小车&a…...

【C生万物】C语言数据类型、变量和运算符

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有…...

CTF--Web安全--SQL注入之‘绕过方法’

一、什么是绕过注入 众所周知&#xff0c;SQL注入是利用源码中的漏洞进行注入的&#xff0c;但是有攻击手段&#xff0c;就会有防御手段。很多题目和网站会在源码中设置反SQL注入的机制。SQL注入中常用的命令&#xff0c;符号&#xff0c;甚至空格&#xff0c;会在反SQL机制中…...

线程池常用的阻塞队列

新任务来的时候&#xff0c;会先判断当前运行的线程数量是否达到核心线程数&#xff0c;如果达到的话&#xff0c;新任务就会被存放在队列中。 不同的线程池会选用不同的阻塞队列&#xff0c;我们可以结合内置线程池来分析。 ● 容量为 Integer.MAX_VALUE 的 LinkedBlockingQue…...

【Java EE】----SpringBoot的日志文件

1.SpringBoot使用日志 先得到日志对象通过日志对象提供的方法进行打印 2.打印日志的信息 3.日志级别 作用&#xff1a; 可以筛选出重要的信息不同环境实现不同日志级别的需求 ⽇志的级别分为&#xff1a;&#xff08;1-6级别从低到高&#xff09; trace&#xff1a;微量&#…...

【网络安全】2024年暗网威胁分析及发展预测

暗网因其非法活动而臭名昭著&#xff0c;现已发展成为一个用于各种非法目的的地下网络市场。 它是网络犯罪分子的中心&#xff0c;为被盗数据交易、黑客服务和邪恶活动合作提供了机会。为了帮助企业组织更好地了解暗网发展形势&#xff0c;近日&#xff0c;卡巴斯基的安全研究…...

SpringMVC-组件解析

一、引子 我们在上一篇文章Spring MVC-基本概念中&#xff0c;为读者解释了如何使用SpringMVC框架&#xff0c;将承接客户端请求的工作从原生的Servlet转移到我们熟知的Controller中。那么我们不禁会好奇&#xff0c;SpringMVC框架到底做了什么&#xff0c;是怎么把请求分发给…...