System V的概念,在上一篇文章“管道”中已经有了基本的讲解。这里就不再过多赘述。简单来讲,System V是一个出现时间较早的通信标准,在现在使用的并不多,原因就是它关注的是“本地进程通信”。即,基本就用于进行本地进程通信,无法实现跨平台进程通信,这也就导致这一标准很难适应当今时代“万物互联”的发展潮流。
System V提供了共享内存、信号量和消息队列三个进程间通信解决方案。这里就只是初步了解一下其中的共享内存和信号量两个方案,消息队列不过多赘述。
管道的通信方式是创建一个匿名/有名管道文件来进行通信,本质上就是利用文件的读写来进行通信。而共享内存通信方案,从名字上看,大家都应该知道这种通信方式和内存有关。
大家应该都知道,一个程序运行起来会在内存中生成进程,为了便于操作系统管理,又会生成进程结构体。 在这个结构体里面有一个结构体指针,指向一块虚拟内存空间,该结构体的内部就是虚拟地址空间的各种划分。然后进程的虚拟地址空间还要通过页表映射到物理内存空间上的指定位置。假设现在又有一个进程,它也会通过同样的操作将自己的虚拟地址空间映射到物理地址空间上。
那么如何让这两个进程实现通信呢?那首要问题当然还是如何让这两个进程看到同一块空间。在共享内存方案中,采取的是在操作系统中申请一块空间,然后将这块空间通过页表分别映射到这两个进程之中,然后将这块空间的起始地址保存在进程结构体中。通过这种方式,就让两个不同的进程拥有了同一块空间。这块不同进程共有的内存空间就叫做“共享内存”。在这个过程中,将创建好的内存映射到进程的虚拟地址空间的步骤就可以叫做“进程和共享内存挂接”。当不再需要这块内存空间的时候,首先需要取消进程和内存的映射关系,即“去关联”。然后才是释放内存。
总的来说,共享内存的原理还是很简单的。
在理解共享内存的概念之前,要先有三个认识。
(1)进程间通信,是专门设计的,用于IPC。
(2)共享内存是一种通信方式,所有想要通信的进程都可以使用这一通信方案。
(3)OS中很可能会存在很多不同的共享内存。因为假设A、B两个进程想通信,C、D两个进程也想通信,那么它们就需要两个不同的共享内存来进行通信。
共享内存的概念也很简单。通过让不同的进程,看到同一个内存块的通信方式,就叫做“共享内存”。
用共享内存完成进程间通信的第一步,就是要向内存申请一块共享内存。要实现这一操作,可以使用shmget()函数。
这个函数一共有三个参数,都是输入型参数。这个函数的第二个参数很好理解,就是要申请的共享内存空间的大小。重要的是第一个和第三个参数。
首先来看第三个参数,就这样看可能不太明白,我们来看看文档中对该参数的解释:
从文档中可以看到,这个参数中可以传四个选项。看到这里,大家应该就很清楚了。这个参数的作用和open()函数的第二个参数是一样的作用,都是“二进制标志位”。有些函数会遇到需要传多个同类型参数以实现不同操作的情况,因此就用宏的方式定义了二进制标志位,通过传不同的标志位让函数实现不同的操作。它的本质其实就是一个个整型。
这个参数的主要作用就是限定申请共享内存的动作。在这几个宏里面,我们当前只需要着重关注“IPC_CREAT”和“IPC_EXCL”两个即可。
“IPC_CREAT”的作用很简单,申请一块共享内存时,如果该共享内存不存在,则创建,存在,则获取该共享内存。比如有两个进程用同一个共享内存进行通信,第一个进程申请了共享内存,第二个进程就无需再申请,直接获取该共享内存的位置即可;
“IPC_EXCL”这个宏是无法单独使用的,必须要搭配使用。如果“IPC_EXCL”和“IPC_CREAT”一起使用,它们的作用就是“申请一块共享内存,不存在就创建,存在就报错返回”。主要用于防止无关进程误用其他进程的共享内存,告诉用户函数调用成功时一定是一个新的shm。注意,在传参时,一定要设置权限。因为创建出来的共享内存其实也是有权限的,如果不设置权限,它就默认为全0,这样进程就无法进行访问了。例如要创建一个权限为0600的共享内存,就可以写成如下所示:
此时查看它的共享内存,就可以看到该共享内存的权限是0600
然后再来看看该函数的返回值。
这个返回值其实就是成功时返回一个数组下标,失败时返回-1。但是它的数组下标和文件的数组下标不同,文件的数组小标都是从0开始的,而它的下标在不同的系统下可能不同,既可能从0开始,也是从1000开始。这也就导致它很难被整合进后端网络服务器当中,在现在很少用。这个数字,大家将其作为一个共享内存的标识符来看即可,未来如果想对共享内存做修改,就可以使用它。
最后再来看该函数的第一个参数。
我们用shmget()函数来获取共享内存,但是,如何标识两个不同的进程获取的是同一块共享内存呢?匿名管道是靠继承来让父子进程拥有同一个管道文件的读写端,命名管道则是让两个不同的进程访问同一个管道文件。那共享内存呢?这个函数中的“key”其实就是申请的共享内存的唯一标识符。key的值是多少不用关心,只要知道这个值是一块共享内存的唯一标识即可,让用户知道不同的进程获取的是同一个共享内存。就好比在学校里面我们各自的学号是多少不重要,重要的是可以通过这个学号来找到唯一对应的人。
既然这个key是共享内存的唯一标识,那就很明显,这个值是不能让用户随便填的。实际上,key值的获取需要调用另一个函数“ftok()”。
该函数的返回值就是要传入shmget()的key值。它的作用是将一个路径名(文件名)和一个项目的标识符转化为IPC key。还是那句话,它返回的key是多少并不重要,重要的是可以通过这个函数来获得一个不会和其他共享内存冲突的唯一标识值。
该函数中的两个参数分别是路径名和项目标识符。路径名必须是一个存在的文件的路径;项目标识符则可以随便写。该函数会对传入的参数进行算法整合,返回一个唯一值。
只要你传入的这两个参数是一样的, 那么它所返回的key值就是一样的。当两个不同的进程的key值是一样的时候,就说明这两个进程所用的是同一块共享内存。
在学习C的时候,想必大家都用过malloc()函数开辟空间。但是大家有没有疑惑过,用malloc()函数时,需要将开辟的空间的类型和大小都传进去。但是用free()释放时,却只需要传一个指针即可。原理很简单,假设用malloc()开辟了4kb的空间,操作系统并不是只开辟4kb,而是会多开辟一些空间,用于存储这块空间上的相关属性,如大小,起始位置,结束位置等,以便于操作系统进行管理。而共享内存也是如此。
在申请共享内存时,操作系统会多申请一些空间,用于创建一个结构体,这个结构体里面就包含了共享内存的相关属性。传入shmget()函数的key值也会被存入这个结构体中。所以当要对共享内存进行操作的时候,并不是对内存块操作,而是对它的结构体进行操作。
此时可能就有人会问了,既然key值是用来标识共享内存的唯一性的值,那shmget()返回的值有什么用呢?这个值其实是用于用户层的,让用户可以根据这个值对共享内存进行操作。shmget()返回值和key值的关系就好比文件中的fd与inode的关系。大家知道,一个文件一个inode,inode就是文件的唯一标识。既然有inode,那为什么还要有fd呢?就好比你本身就有一个身份证号,那为什么你在学校里面还要有一个学号,在公司里面要有一个工号呢?道理是一样的,就是为了便于管理。所以shmget()的返回值和fd都是为了方便用户在用户层对其进行操作。
要查看IPC资源,直接输入“ipcs -m”命令即可。
(1)共享内存的生命周期是随OS的,不会因为进程的结束而被释放。
在上图中就申请了一块共享内存。但是可以看到,当shm_server程序结束后,再用ipcs -m查看共享内存,它依然存在。这也就说明共享内存的生命周期是不随进程的结束而结束的。
如果要释放共享内存,在linux中就要使用“ipcrm -m shmid”命令。要记住,删除时所输入的不是共享内存的key值,而是它的shmid。因为key值不是给用户使用的,而是让操作系统标识它的唯一性, 方便管理所设置的。
在上面,仅仅只是说了在linux中可以用命令行的方式释放共享内存。而如果要调用函数来释放共享内存,就需要使用shmctl()函数。
该函数可以控制共享内存。注意,这里说的是控制,而不是删除。也就是说这个函数可以对共享内存进行操作,其中就包括删除。
该函数的第一个参数shmid,就是要控制的共享内存的id。
第二个参数cmd,就是控制方式。打开文档,查看对该参数的解释:
看到这些,大家应该很清楚,这其实就是宏,是一个“二进制标志位”。当然,这里的可执行操作并没有截全,因为在这里我们所需要的仅仅只是释放共享内存,即第三个标志位“IPC_RMID”。将这个标志位传入函数中,就可以释放对应的共享内存。
第三个参数是一个结构体指针,一般是需要对共享内存的属性进行操作时才需要传入。如果不需要,传入nullptr即可,此时我们仅仅只是用该函数进行删除,所以第三个参数也只需要传入nullptr。
上面已经介绍了共享内存的创建和删除函数。那如何进程与进程通过共享内存关联呢?其实就是需要使用shmat()函数来实现。
这个函数的第一个参数是要关联的共享内存的id。
第二个参数可以指定要将这块共享内存映射到进程的虚拟地址空间的指定位置。在现阶段大家几乎用不到该参数,直接设置为nullptr。因为大家并不知道要将共享内存映射到哪里,交由操作系统执行即可。
第三个参数是访问方式。一般设置为0即可。因为设置为0时就默认可以读写该共享内存空间。
最后是它的返回值。可以看到,它的返回值是void*。这个返回值大家应该比较眼熟,因为C中的malloc()函数的返回值就是void*。上文中说过了,共享内存需要通过页表映射到进程的虚拟地址空间上,然后将映射好的空间的起始地址返回给进程的结构体。所以,这个函数的返回值其实就是一个指针,指向进程虚拟地址空间上该共享内存的起始位置。但是因为该函数并不知道创建的共享内存映射到了哪个位置,存储哪个类型的数据,所以这里的返回值是void*。如果关联失败,就会返回-1.
在释放共享内存之前,最好先将进程与共享内存的关联去除。去关联的函数是shmdt():
该函数实用很简单。它的参数就是shmat()的返回值。
该函数在去关联成功时返回0,失败时返回-1
有了上面的知识后,就可以自己写一个简单的demo代码来实现两个不同的进程通信了。
shm_comm.hpp文件:
#ifndef COMM_HPP
#define COMM_HPP
#include
#include
#include
#include
#include
#include
#include
#define PATH_NAME "."
#define PROJ_ID 0x11
#define CAPACITY 4096
key_t GetKey()//获取key值
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key == -1)//key等于-1时,创建失败
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return key;
}
int CreaterShmHelper(key_t k, int flags)//给下面的两个函数提供共享内存获取
{
int shmId = shmget(k, CAPACITY, flags);
if(shmId == -1)//返回-1时表示创建失败
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return shmId;
}
int CreateShm(key_t k)//创建共享内存
{
return CreaterShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);//不存在创建,存在则报错返回
}
int GetShm(key_t k)//获取共享内存
{
return CreaterShmHelper(k, IPC_CREAT);//不存在创建,存在返回
}
void* AttachShm(int shmid)//用共享内存关联进程
{
void* mem = shmat(shmid, nullptr, 0);//出错是返回的是(void*)-1。在64位系统下,指针的占8个字节,因此无法强转为int。
if((long long)mem == -1L)//但是可以强转为同样是8字节的long long。后面的“-1L”其实就相当于(long long)-1。因为单数字是没有意义的。
{ //必须要带有类型。如果单写100,是没有意义的,但是编译器会将它视为整形。这里的100L就是让编译器将100视为
std::cerr << errno << ":" << strerror(errno) << std::endl;//long long类型。这种写法可以看成100$,指的是100美元,而(美元)100
exit(3); //指的也是100美元。
}
return mem;
}
void DetachShm(void* start)//去除进程与共享内存的关联
{
if(shmdt(start) == -1)
std::cerr << errno << ":" << strerror(errno) << std::endl;
}
void DelShm(int shmid)//释放共享内存
{
if(shmctl(shmid, IPC_RMID, nullptr) == -1)//返回-1就是释放失败
std::cerr << errno << ":" << strerror(errno) << std::endl;
}
#endif
shm_server.cpp文件:
#include "shm_comm.hpp"
int main()
{
//获取key值
key_t k = GetKey();
printf("k:%x\n", k);
//创建共享内存
int shmid = CreateShm(k);
printf("shmid:%d\n", shmid);
//将进程与共享内存关联
char* start = (char*)AttachShm(shmid);//将共享内存空间视为一片存储字符的空间
printf("attach success, address start:%p\n", start);
//两个不同进程的通信操作(shm_server接收数据)
while(1)
{
std::cout << "shm_client message:" << start << std::endl;//将共享内存的数据一次性全打印出来
sleep(1);
}
//将进程与共享内存去除关联
DetachShm(start);
//释放共享内存
DelShm(shmid);
std::cout << "delete success" << std::endl;
return 0;
}
shm_client.cpp文件:
#include "shm_comm.hpp"
int main()
{
//获取key值
key_t key = GetKey();
printf("key:%x\n", key);
//用key获取共享内存
int shmId = GetShm(key);
//与共享内存相关联
char* start = (char*)AttachShm(shmId);
printf("attach success, address start:%p\n", start);
//两个不同进程的通信操作(shm_client进行写入)
const char* message = "hello shm_server, 我是shm_client进程,正在与你通信";//发送的信息
pid_t id = getpid();//进程pid
int cnt = 0;//计数
while(1)
{
sleep(1);
snprintf(start, CAPACITY, "%s[pid:%d][cnt:%d]", message, id, cnt++);//将数据传输到共享内存,snprintf会默认
} //添加\n,所以无需自己添加
//与共享内存去除关联
DetachShm(start);
return 0;
}
写起来其实很简单,就按照“生成key值->用key值申请共享内存->让进程与共享内存相关联->执行通信操作->去除进程与共享内存的关联->释放共享内存”的思路就可以完成了。
(1)优点:
在所有进程间通信的方式中,共享内存通信方式是最快的。因为共享内存是直接让不同进程看到同一块内存,利用这块空间进行通信。
例如与管道相比。管道要完成一次通信,首先需要将数据拷贝到缓冲区,然后再将缓冲区内的数据拷贝到管道文件,再将管道文件内的数据拷贝到读端进程的缓冲区,最后读端进程从缓冲区内将数据拷贝到输出端。这里面一共需要4次拷贝。而共享内存则只需要将数据从进程中拷贝到共享内存,再让另一个进程从共享内存读数据即可。只需要进行两次拷贝
当然,大家也知道,在将数据拷贝到缓冲区和将缓冲区内的数据拷贝到另一个进程时时,并不是直接拷贝的,而是要先将数据拷贝到对应的输入输出流的缓冲区内,然后再拷贝到缓冲区内。如果算上这两次拷贝,那么管道就是6次,共享内存就是4次。
(2)缺点:
共享内存的缺点就是共享内存中没有同步与互斥机制,不会对数据进行保护。
在学习管道的时候大家都知道,管道内的数据在被读走的时候,管道文件内已经读取过的数据就会被清除。如果写端停止写入数据,当读端将管道内的数据读完后就会阻塞等待,当写端重新写入数据时才会被唤醒并继续读取数据。
但是共享内存不同,它没有任何的保护机制。如果写端没有再写入数据,那么读端的进程就会反复读取写端上一次输入的内容。
在上图中,一个进程会不断地向另一个进程发送数据。发送的数据用编号进行了标注。可以看到,当左端写入数据的进程结束后,右端读取数据的进程还在持续运行并打印同一条消息。这条消息就是左端进程写入的最后一条消息。
在共享内存中,虽然可以随意申请空间大小。但是建议申请4KB的整数倍大小。如果你想申请4500byte大小也是可以,但是操作系统会根据你所申请的空间大小,向上增长的给你空间。以上面的4500byte为例,这个大小大于4KB,小于8KB,所以操作系统实际上会为你申请8KB的空间。但是虽然为你申请了8KB空间,但你只能使用4500byte空间。只能使用申请空间大小的机制是为了方便用户使用,让用户申请多少就用多少;而向上增长申请4KB整数倍空间是因为系统分配共享内存是以4KB为单位分配的,它是内存换分内存块的基本单位。
这个内容就不再过多讲解了。大致了解一下它的概念即可。
消息队列提供了一个从一个进程向另一个进程发送一块数据的方法。
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
从特性上来看,IPC资源必须删除,否则不会自动清除,除非重启。这一点和共享内存是一样的,它们的IPC资源的声明周期都是随内核的,不随进程。
消息队列的通信方式,其实就是系统在内存中创造出一个一个的节点,以队列的形式链接起来,当用户层有进程需要通信时,写数据的进程就可以将数据以节点的方式写入到队列中。但是这两个进程是以同一个队列完成读写的,所以需要有一个标志类型的字段type,这个type表示的就是数据块的类型。这个类型一般是自定义的,例如由进程1写入的数据块标为0,由进程2写入的数据块标为1.当着两个进程读取数据时,进程1就只会读取类型为1的数据,进程2只会读取类型为0的数据,以免读到自己写的数据。
当然,因为进程中可能存在多个进程使用消息队列通信,所以方便操作系统管理,消息队列也会有自己的结构体,里面保存了消息队列的相关数据。
要创建一个消息队列,直接调用msgget()函数即可。
这里面的参数和共享内存中的一样,key是调用ftok()后得到的唯一标识符,msgflg则是用什么方式创建,同样是以宏的方式存在的。
要控制一个消息队列,就需要调用msgctl()函数。
参数msqid是msgget()的返回值,标识要控制哪一个消息队列。cmd也是传宏,表示用什么方法控制消息队列,其中就包括删除。而buf参数是一个结构体指针,用于获取消息队列的属性,如果不需要,则设置为nullptr。
要向消息队列中写数据,需要调用msgsnd()函数。
第一个参数是要向哪个消息队列放数据。第二个参数是一个类型指针。前面说了,消息队列传数据都是按照数据块的方式传输的,所以这里msgqp参数就是要传的数据块的地址。第三个参数是要传入的数据块的大小。
传入数据的类型需要定义以下结构体来传入:
创建一个结构体指针,将里面的type修改为要传入的数据类型。第二个里面就是传入的数据
要从消息队列读数据,就调用msgrcv()函数。
第一个参数是要从哪个消息队列读数据。第二个参数是一个输出型参数,需要定义一个以下结构体来进行接收数据:
第三个参数是要读取的数据的大小。第四个参数是要读取的数据的类型,这个类型是在msgsnd()中设置好了的。
消息队列这一通信方式,在当前时代已经很少使用了,所以只需要大致了解一下即可,无需过多深入。
信号量的本质,其实就是一个“计数器”。它可以用于表示公共资源中资源数量的多少。
在了解信号量之前,先要了解一下的概念。
首先我们知道,要完成进程间通信,就必须要让不同的进程看到同一块空间,而这块空间就被叫做“公共资源”。
但是当不同进程访问同一块公共资源时,就可能出现“数据不一致”的问题。例如“共享内存”的通信方式,共享内存中的公共资源是不会被保护的,它的数据可能因为某些情况导致数据被覆盖或者反复读取,这种情况就会让进程读取错误的数据。
为了解决进程间通信时公共资源中的数据可能出现数据不一致的问题,就需要将这块公共资源保护起来。而那些被保护起来的公共资源,就被叫做“临界资源”。例如管道通信中,管道文件就会被保护,它里面的数据在被读取完后就会让读端进入阻塞不再读取,并且每次读取完后都会清空数据,避免进程读到错误数据。此时,这些管道文件就是“临界资源”。
而这些临界资源又会被进程通过它对应的代码来访问。此时在进程中那些访问了临界资源的代码就被叫做“临界区”,而那些未访问临界资源的代码就叫做“非临界区”。例如在一个进程的代码中,它用管道进行通信,其中向管道写数据读数据的write()、read()代码就是临界区;而那些诸如创建子进程、等待子进程结束的代码就是非临界区。
现在大家知道了公共资源需要被保护,那么如何进行保护呢?这其实就是通过“互斥与同步”机制完成的。互斥,简单来讲就是公共资源中,同一时刻只允许一个进程访问。而“同步”,在这里讲的话大家不太容易理解,就不过多赘述。
同时要知道,“互斥与同步”机制的实现,都是依赖于另一个机制,即“原子性”的。原子性的概念其实可以看成两个极端,即“要么不做,要么就做完”。举个例子,如果一个进程要向公共资源写数据,那么该进程要么不写,要么就要将所有要写的数据一次性写入。再比如玩游戏,如果你喜欢玩一款游戏,那么从原子性上看,你只能有要么不玩,要么就将水平玩到最高这两种状态之一。可以看成一种只能从“有无”之间选一个的二态。
在这之前,先来看一个例子。在大家的生活中,想必大家都看过电影。当我们要去电影院看电影之前,就需要先买票。买票这一行为,其实就是在预定座位。表示在这场电影的放映厅里面有我们的一个位置。当买到票时,就说明了两个问题,一个是在买票时,放映厅里面还有位置供我们预订,另一个就是在这个放映厅里面有属于我们的一个座位。在放这场电影时,这个座位是属于我们自己的,哪怕放电影时我们因为某些原因不在,这个座位依然属于我们,直到这场电影结束。
从上面的例子中,就可以得出两个个结论。第一个是当我们需要需要某种资源时,是可以进行预订的。第二个结论就是,只有当这些资源还存在剩余时,才能进行预订。
例如电影院中有100个位置,那么就可以被预订100张票,每张票上都会有票号和座位号,票号用于标识哪张票,座位号用于标识哪个位置。
上面说了,信号量其实就是一个计数器,用于记录公共资源中资源的数量的多少。在这里,就可以将信号量看成是售票机,它记录了电影院还有多少票,当票数为0时,表示没有资源可以预订。
那为什么会存在信号量这一机制呢?原因很简单,上面说过了,公共资源需要保护,其中的一个保护机制就是“互斥”。该机制保证了一个公共资源在同一时刻只能有一个进程访问。但是,如果一个公共资源需要同时被多个进程访问,那么这一机制就会导致访问效率大大降低。为了提高访问效率,就提供了将一份公共资源划分为多个子资源的模式,而信号量,就用于标识还有多少个子资源。
通过划分子资源的方式,就让多个进程可以同时访问一份公共资源中的不同子资源。但是子资源由信号量管理,所以进程要访问子资源,就需要申请信号量,当申请成功时才能访问,申请失败就表示所有子资源都被其他进程使用,此时由于互斥机制,进程无法访问子资源。但是要知道,不同进程要申请同一个信号量的前提是这些进程看到的是同一个信号量。同一个信号量可以被不同进程看到,就说明它自身也是公共资源。既然信号量也是公共资源,它也就需要有保护机制,以避免被随意修改。而信号量的保护自己的安全依靠的就是“原子”。即信号量的++和--操作是原子的。
同时,因为信号量本身也是一个公共资源,所以它也就被划分到了进程间通信。
既然可以申请信号量,那么就可以释放信号量。所以,在进程使用完子资源后,就需要释放信号量,即++信号量。其中,申请信号量被称为“P操作”,释放信号量被称作“V”操作。
通过申请信号量的方式,可以将一个公共资源划分为多个子资源。如果申请的信号量是1,则表示该公共资源在同一时刻只能有一个进程访问。这也就说明进程访问的是整体公共资源。而这为1的信号量被叫做“二元信号量”。如果为一个未被保护的公共资源申请一个信号量,就使得该公共资源处于“互斥”状态。
到了这里,大家可能就有了一个问题。虽然信号量可以用于标识可用子资源的多少。但是进程申请信号量时仅仅是获得了访问子资源的许可,并没有规定访问哪个子资源。这就可能出现有两个进程都获得了访问许可,但是却访问的是同一个子资源的情况。这种情况确实可能存在。但是就好比在买票时,我们的票上不仅有票号,还有座位号,用于标识你的座位,防止与其他人冲突。信号量在被申请成功时,也会通过一些方式标定该进程可以反问的子资源是哪一个,当子资源被标记后,就不会再被分配给其他进程访问。这一行为一般是由程序员自己控制的。
为一块公共资源申请信号量可以用semget()函数。该函数的第一个参数想必都不陌生,就是用ftok()形成的公共资源唯一标识符。第二个参数表示要为该空间申请多少信号量。第三个参数大家应该也很熟悉,传的是宏,表示申请方式。
信号量申请成功时,会返回信号量的id,用于标识信号量。
semctl()函数可以用于控制信号量。其中第一个参数是信号量的id,即semget()的返回值。第二个参数表示该信号量中的那一个信号量,是一个数组下标。例如我们申请了一个信号量,该信号量有10个成员,要对里面的第一个成员操作,就把下标0传进去。第三个参数就是要控制信号量的方式,传的也是宏,其中就包括删除。
进程要访问公共资源,就需要申请信号量。当进程访问完公共资源后,就需要释放信号量。而申请/释放信号量可以用semop()函数。
其中第一个参数semid,就是申请信号量时semget()的返回值。
第二个参数是一个结构体,可以看看文档里面:
通过文档可以看到,该结构体中至少有以上三个参数。第一个参数sem_num表示你要申请/释放的信号量的下标;第二个参数表示你要进行什么操作,传1为P操作,即申请信号量,传-1为V操作,即释放信号量。第三个参数sem_flg一般填0即可。
最后,该函数的第三个参数nsops是表示你要操作的信号量个数。只操作一个就传1。该函数是允许对多个信号量同时操作的,例如你要操作10个信号量,那么你就创建10个struct sembuf结构体对象,然后把它们放到一个数组里面传入,再将nsops设置为10即可。
首先要知道,无论是共享内存、消息队列还是信号量,为了方便操作系统管理,它们都必定要生成自己的结构体。现在可以分别查看shmctl()、msgctl()、semctl()三个函数的文档,里面就写了它们的结构体:
从这里可以看出,它们的结构体命名都是非常相似的。这也就说明了在system V标准中,所接口的命名风格其实都是有标准的。
大家应该都知道,在一个结构体中,它的第一个成员的地址,在数字上和结构体对象是一样的。而在底层上,操作系统为了更好的管理这一标准所申请的各类公共资源,其实是将它们的结构体放在同一个数组里面的。也就是说,system V标准中所申请的公共空间,无论是共享内存、消息队列还是信号量,它们的结构体都是放在一起的。
此时大家可能就有疑问了,既然它们放在一起,那么如何访问它们呢?其实就是通过类型强转的方式访问。将该数组中的每个元素都根据它的类型进行强转。但这又会延伸出一个问题,就是操作系统如何知道数组中的每个元素的类型呢?这其实也很简单,例如将数组的类型定义为struct ipc_perm,这个结构体中保存两个参数,一个是类型,一个是数据。当要访问某个元素时,就根据这个结构体中存储的类型,来对数据进行特定类型的强转即可。通过这一方式,就可以让同一个类型的数组存储不同类型的数据。这其实就是多态的思想,根据传入类型的不同,调用不同的数据。