我们在介绍进程间间通信的时候说到了共享内存,共享内存有一个特点就是共享内存并没有提供同步和互斥机制的,是需要我们自己来实现共享内存访问的同步和互斥。
首先要明白什么是同步与互斥。互斥就是:进程对临界资源的同一时间的唯一访问性。同步就是:进程对临界资源的顺序访问关系。通俗来说互斥就是一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。而同步就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
这里之前看到一个例子:我们都使用过打印机,但是我们有很多进程都可能会去调用我们的打印机,wps可以,word可以等等很多进程都可以去调用,但是打印机只有一个,所以操作系统就想到了一个办法,在操作系统中专门开辟了一块空间,谁想要打印东西按照先后顺序排队放在这里,但是这样之后就出现了一种冲突。Wps就来找打印机,为什么你打印了我前两个文件第三个xxx没有打印出来呢?打印机说我没有看到你这个文件啊,wps很纳闷我明明把我的文件放到了3号队列里怎么会没有呢?之后在操作系统的帮助下才发现了根本原因,是word在这个过程中查了一脚,当时word和wps同时进来打印,word读取到这块空间里边该往队列3里边放东西了,他就记下了,但是这时候word进程里边突然引起了一次时钟中断,导致操作系统感觉word这个进程只是想打印个东西,已经执行这么久了不该它运行了,就切换到了wps,wps这时候也看到了3位置是空的,就直接把文件放到这个3位置就走了,这时候再回来到word的时候,就也把文件放到了3号槽这个位置,这时候wps自身的文件就被覆盖掉了,打印机是什么都察觉不出来的。所以就出现了问题。
很明显这两个进程在访问我们临界区的时候出现了问题,这次是wps下次可能就是word,这种对共享变量,共享内存,共享资源进行访问的程序片段就是我们平时说的临界区。所以代码在进入临界区的时候一定要做好同步和互斥操作。
信号量就是用来实现我们上边说到的同步和互斥操作的,信号量说白了就是认为定义了一个变量,它只能被两个标准的原语wait(S) 和 signal(S) 来访问,也可以记为“P操作”和“V操作”。
信号量可以理解成一个计数器,用它来标志,当这个数值大于0的时候表示现在可以用的资源数,当数值等于0的时候代表这时候没有可用资源也没有在等待的进程,当数值小于0的时候,这时候数值的绝对值代表着等待的进程数,这是很容易理解的。
p操作进来就让我们能访问的资源值减去1,如果资源值小于0了那说明这时候没有资源可以访问,那你就去排队等着。
v操作就是去释放我们的资源,让我们可以访问的资源加1,如果这时候数值小于等于0那说明,有进程在等着资源,那你就要去唤醒等待的进程来访问刚刚释放掉的资源。
我们通过程序来实现信号量的互斥操作
//这是实现信号量互斥操作的程序
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define IPC_KEY 0X12345678
union sem {
int val;//信号量数值
struct semid_ds *buf;//内核所提供的结构体
unsigned short *array;//
struct seminfo *_buf;
};
void sem_P(int id)
{
struct sembuf buf;
//sembuf结构体包含三个变量
//num代表信号量的编号
//op代表一次pv操作改变的值一般是-1或者1,这里是p操作所以是-1也就是让资源量变少的
//flg有两个选项SEM_UNDO是让操作系统跟踪信号,并在进程没有释放改信号而终止的时候,操作系统将其释放
buf.sem_num=0;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
semop(id,&buf,1);
//semop是用来创建和访问一个信号量集
//第一个是semop的操作信号量表示码也就是semget的返回值
//第二个是指向一个结构数值的指针
//第三个是信号量的个数
}
void sem_V(int id)
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=1;//这里是V操作是让信号量加1的所以这里是1
buf.sem_flg=SEM_UNDO;
semop(id,&buf,1);
}
int main()
{
int semid=semget(IPC_KEY,1,IPC_CREAT|0664);
//semget创建一个信号量
//第一个参数是信号量的标识
//第二个参数是要创建的信号量个数
//第三个参数和之前创建文件管道等等的作用是一样的
if(semid<0)
{
perror("semget error\n");
return -1;
}
union sem semval;
semval.val=1;
semctl(semid,0,SETVAL,semval);
//semctl函数给信号量设置初值并且只能设置一次
//第一个参数是信号量的标识
//第二个参数是操作第几个信号量
//之后的参数是不固定的,是用来表示对信号量要做些什么的操作
//SETVAL代表设置信号量的数值,STEALL代表设置所有信号量的数值这时候第二个参数将被忽略
//如果要获取信号量的数值第四个参数就是结构体,如果要给信号量设置初值那么放得就是值的联合体
int pid=-1;
pid=fork();
if(pid<0)
{
perror("fork error\n");
return -1;
}
else
{
if(pid==0)
{
sleep(1);
while(1)
{
sem_P(semid);
//因为我们设置的是一元的信号量,所以在这里进行p操作的时候信号量的数值就会变成0
//这时候其他进程就不能再去访问
printf("A");
fflush(stdout);
sleep(1);
printf("A");
fflush(stdout);
sem_V(semid);
//我们操作完成后释放掉我们的信号量这时候其他进程就可以操作了
}
}
else
{
while(1)
{
sem_P(semid);
printf("B");
fflush(stdout);
sleep(1);
printf("B");
fflush(stdout);
sem_V(semid);
}
}
}
return 0;
}
可以看出来在最初的时候我们让打印A的进程暂停了1s这段时间都是在打印的B等这段时间结束之后,就变成了我们的AABB有规律的在进行打印。这么看是感觉不出来的,我们现在通过一个没有互斥操作的程序来打印一下。
这就能看出来,我们两个B还没有打印完就去打印A了,两个A也没打印完因为进行了中断就去打印B了。这就出现了我们最初的时候提到的问题。
用完之后我们还是把它删除
这个是必须要删除的,无论进程怎么样他是不会自动清除的,除非重启系统,所以我们还是要养成良好的习惯所有的ipc都是要进行清除的。