什么是信号量
信号量的本质是一个计数器,通常用来表示公共资源中资源数量多少,公共资源是指可以被多个进程同时访问的资源,访问没有被保护的公共资源时可能出现数据不一致的问题,比如说一个进程对公共资源执行一些写操作,当写操作完成了一半时另外一个进程就对该公共资源进行了读取,那么这个时候就会出现数据不一致的问题,这里先暂停一下我们来回顾一下这里的逻辑:我们为什么要让不同的进程看到同一份资源呢?因为我们想要实现通信从而来实现进程之间的协同,又因为进程之间具有独立性没有办法让进程之间直接通信,所以我们得让进程看到同一份资源,这是我们提出的解决问题的方法但是这个方法又会引入了新的问题:数据不一致,所以我们得将实现通信的公共资源保护起来,我们把被保护的公共资源称为:临界资源,那么未来我们要干的事情就和如何保护公共资源有关,在保护公共资源的时候就会使用到信号量这个东西。我们程序的大部分资源都是独立的,资源(内存资源,文件资源,网络资源)是要被进程使用的,那资源又是如何被进程使用的呢?一定是该进程里有对应的代码来访问这部分临界资源,我们把访问临界资源的代码称为临界区,访问非临界的资源的代码称为非临界区。看到这里想必大家应该知道了这里的逻辑,我们写的程序会访问公共资源,为了保护公共资源的安全,所以得对这部分资源进行保护,保护得使用信号量(如何使用我们以后再说)保护的方法就是互斥和同步,同步的概念我们后面再详细的解答,互斥的意思就是:我访问的一部分资源或者做某件事的时候你必须得等,同样的道理你在访问某部分资源或者做某件事情的时候我也必须得等,你做完了我或者其他人才能继续做那么我们就把要么做就做完,要么就不做的这两种情况称为:原子性,而信号量的作用就是通过原子性来保护公共资源。
为什么要有信号量
我们可以通过电影院买票的例子了解信号量,电影院的买票机制实际上是对放映厅中的座位进行预定机制,当我们想要某种资源的时候就可以对该资源进行预定,比如说我买了电影票电影院就会帮我对某个位子进行预定,即使我们不去电影院但是那个位置上面不能有其他人,所以电影院得有机制来保证电影票不能多卖和错卖,那么公共资源也是同样的道理,公共资源有时候会作为一个整体来进行使用,有时候也会变为一个一个的资源字部分来进行使用,那么我们人就相当于操作系统中的进程,电影院的每个座位就相当于共享资源的一小部分,人通过购买电影票来对座位进行预定就相当于进程通过申请信号量从而对共享资源进行预定,如果对信号量的申请失败了那么信号量就不允许该进程访问共享资源从而对共享资源进行了保护。信号量的本质是计数器,申请信号量就得对计数器进行减减从而预定了公共资源,释放公共资源之后就得对信号量进行加加,所有的进程在访问公共资源之前,都必须先申请信号量,申请信号量的前提是所有进程必须先得看到同一个信号量,所以这里就可以推出信号量本身就是公共资源,既然是公共资源那么就一定存在数据安全问题,所以信号量也得保证自己的数据安全,所以信号量的++和–操作也必须得是原子的,我们将信号量的增加称为p操作,信号量的减少是称为v操作。如果一个信号量的初始值为1的话我们就称之为二元信号量,也就是说一个资源要么你不能访问,要么就只有你能够访问,所以也可以将其称为互斥信号量。这里大家得注意一点信号量申请成功了并不代表我们接下来就能够访问共享资源,申请信号量成功只能说明共享资源中可能有个资源属于我,这就好比我们购买高铁票的时候可能是先购票成功了,然后过了一会出票的时候才知道自己具体坐着哪个车厢的哪个位置上,所以申请信号量成功就相当于购票成功了,但是具体要访问哪个资源还是不知道,所以这一部分的工作还得交给程序员来完成
信号量有关的函数
首先我们了解一下semget函数:
这个函数的作用就是用来申请一个能够被所有进程看到的信号量,第一个参数就是key,第二个参数用来表示申请了几个信号量如果你想申请10个信号量就传递10给第二个参数,semflg就是之前的标识位,申请成功了就返回信号量集合的标识符,申请失败了就会返回-1,接下来就是信号量控制函数
第一个参数表示对信号量的id也就是semget函数的返回值,如果你相对哪个信号量进行操作就传递对应信号量的标识符,因为信号量会存在多个,所以第二个参数表示的意思就是要操作的信号量的下标,如果想对标识符为semid的第二个信号量进行操作,那么这个参数就应该传递为1,cmd就是标识符不同的标识符有不同的功能,比如说想要删除信号量就可以传递信号量IPC_RMID, 如果想要执行其他的操作就可以传递其他的标识符:
根据前面的讲解我们知道信号量是一个计数器,所以他一定存在对应的加减计数器的操作:
第一个参数表示对哪个信号量进行操作,第二个参数是一个结构体,这个结构体里面存在三个参数:
第一个参数表示你相对多个信号量中的哪个信号量进行操作,假设只有一个信号量那么这个参数传递的值就是0,第二个参数表示你要执行的操作,这个参数可以设置的值为1或者-1,如果值为1就是对指定的信号量进行加加,如果该该值为-1就表示对指定的信号量进行减减,第三个选项就直接设置为0就可以了,那么这就是第二个参数的作用,第三个参数表示要对几个信号量进行操作,如果想一次性对10个信号量进行操作那么第三个参数就可以传递10,并且第二个参数就传递一个结构体数组那么这就是信号量有关的操作,这里我们在后面的多线程保护会继续的给大家进行讲解。
IPC资源的组织方式
共享内存有自己的属性所以有对应的结构体来描述共享内存,我们可以通过shmctl来进行查看:
这里有两个结构体,两个结构体共同描述共享内存的属性,与共享内存相似的还有消息对量他也是一个共享资源,所以也存在对应的结构体来描述这一部分资源,那么这里可以使用msgctl函数来进行查看:
可以看懂这里也是两个结构体一起描述的该资源的属性,我们刚刚学习了信号量,信号量的本质就是一个计数器也是一个共享资源,所以他也存在对应的结构体来描述他的属性,那么这里就可以使用semctl来进行查看:
可以看到这里也是两个结构体描述的共享资源,大家仔细的观察一下可以看到描述这三个共享资源的两个结构体中,都有一个名为ipc_perm结构体,并且另外一个结构体中的第一个字段都是ipc_perm结构体对象,所以这就可以看出这就是system V标准的进程间通信,所谓的标准就是大家使用的方式,接口的设计,数据结构的设计都必须符合该标准,那操作系统又是如何管理这些数据呢?比如说我申请了5个共享内存,然后又申请了3个消息队列,最后又申请了4个信号量,那操作系统如何管理这些资源的呢?首先我们可以知道每个共享资源的结构体中的第一个字段都是ipc_perm,并且ipc_perm结构体中存在一个字段来记录key值从而保证数据的唯一性:
那么操作系统要想管理不同的共享资源就只用创建一个ipc_perm的数组就可以了,比如说下面的图片:
我们没申请一个共享资源操作系统就会创建对应的结构体,比如说申请一个共享内存一个消息队列一个信号量,那么操作系统就会创建对应的结构体对象,比如说下面的图片
因为这三个结构体中的第一个元素都是ipc_perm所以我们可以让数组中的元素指向三种结构体中的第一个成员:
因为结构体中的第一个元素的地址和该结构体的地址是一模一样的,只是地址的类型不一样,所以当我们想要访问对应的属性时就可以通过数组来获取对应的地址,然后将地址的属性进行修改就行,比如说数组中的第一个元素指向的是共享内存的结构体,那么我们就可以通过数组的下表访问到具体的地址:perms[0]
,然后就直接进行强转便可以访问到结构体中的其他元素:(struct shmid_ds*)perms[0]
,那么通过这样的方法就可以将不同的结构体资源通过一个数组来进行保存 ,那么这里大家可能还有点疑惑,计算机怎么知道哪个下表的元素存储的是哪个资源的结构体呢?那么这里大家就不用担心我们可以创建一个结构体,结构体中的第一个元素为整形用来表示数据的类型,第二个参数就是一个ipc_perm的指针,用来指向数据结构体中的第一个元素,比如说下面的代码:
struct myIPC
{int type;struct ipc_perm*;
}
这样我们就从维护一个指向ipc_perm的指针数组变成维护一个myIPC的结构体数组,所以这里大家不用担心,那么看到这里想必大家对这个非常的眼熟,好想和c++中的多态类似,没错这就相当于c语言实现的多态,当年c++实现多态的时候可能思路就来自于此,ipc_perm就相当于c++中的基类,那三个共享资源的描述对象就相当于派生类,我们通过派生类的指针指向派生类的第一个元素就然后通过强制类型转换就可以访问派生类的其他成员和属性,那么这就是IPC资源的组织方式。