目录
写在前面的话
一些概念的理解
信号量的引入
信号量的概念及使用
写在前面的话
System V信号量是一种较低级的IPC机制,使用的时候需要手动进行操作和同步。在现代操作系统中,更常用的是POSIX信号量(通过sem_*
系列的函数进行操作)或更高级的同步原语(如互斥锁、条件变量等)来实现进程间通信和同步。所以这里只说一下大概的使用过程,不过多详细解释。后面我们会在POSIX信号量中详细讲解。
一些概念的理解
我们上一篇文章讲了System V共享内存,我们可以知道:
为了让进程间通信 ---> 需要让不同的进程看到同一份资源 ---> 包括之前所有的通信方式,都是优先解决一个问题:让不同的进程看到同一份资源。
但是让不同的进程看到同一份资源,比如共享内存,会带来一些时序问题,例如写方只写到一半还没写完,读方就读走了,即缺乏访问控制,会造成数据不一致问题。
管道内部OS已经处理好这些了,所以不需要关心这些时序问题。
这里,我们
1.把多个进程(执行流)看到的公共的一份资源 叫做 临界资源
2.把自己的进程,访问临界资源的代码 叫做临界区。
例如上一章的例子:
所有的代码只有这两行访问临界资源shmaddr了,所以叫做临界区.
使用共享内存时,多个执行流,运行的时候互相干扰。主要是我们不加保护的访问了同样的资源(临界资源)。在非临界区,多个执行流不相互影响的。
3.为了更好地进行临界区的保护,可以让多执行流在任意时刻,都只能有一个进程进入访问临界区,这就叫做互斥。这个互斥后面会在多线程进行详细的讲解.
信号量的引入
先来说个例子:我们在去看电影时,一定要有座位(放映厅里的一个资源) ,这个座位是不是只有你坐在上面才属于你呢? 很显然不是的,我要先买票,我只要买了票,我就拥有了这个座位,此时不管我人在不在那里,那个座位都是属于我的。
所以买票的本质:对座位的 |预定| 机制。
相应的,每一个进程想进入临界资源,访问临界资源的一部分,不能让进程直接去使用临界资源(不能让用户直接去电影院抢占座位),而是先得申请 信号量(先得买票)。
信号量的本质是一个计数器,类似int count = n(不太准确例子,目前先这样理解).
所以申请信号量:
1.申请信号量本质是:让信号量计数器--
2.只要申请信号量成功, 临界资源内部一定预留了你想要的资源 --- 申请信号量其实是对临界资源的一种预定机制。
3.同样地,临界资源使用完成需要释放信号量,本质是信号量++.
假设一个临界资源中有10份小资源,这个时候 有11个进程,那么前10个进程访问临界资源,申请信号量成功,最后一个进程由于临界资源中资源都在被使用,所以只能阻塞,等待里面进程的资源使用完毕。
上面一直在说信号量,那信号量到底是什么呢?
上面提到了信号量是一个计数器,那这个计数器是什么呢?
首先计数器肯定不可能是局部变量,这样计数器就和每个进程联系不起来了,因为每个进程都有自己的计数器,这就很荒谬了.
那如果是全局变量,父子进程在申请信号量时,会发生写时拷贝,导致这个变量父子进程各自一份,也不太行。
那就假设让多个进程看到同一份n,(即n在共享内存中),然后此时大家再申请信号量可以吗?
答案也是不可以的.
首先我们要知道,计算是在CPU中进行的,而数据n保存在内存中.
CPU执行指令的时候,需要做以下三步:
1.将内存中的数据加载到cpu的寄存器
2.n--(分析&&执行指令)
3.将cpu修改完毕的n写回内存。
这看起来没什么问题,但是执行流在执行的时候,在任何时刻都可能被切换 。
被切换的时候,会带走自己的上下文数据(包括n),然后再被切回来的时候,再把自己的上下文数据写入到cpu的寄存器中,继续执行。
这样就有了一个问题:
假设有10个进程,分别为1,2,3,4...,9,10,而临界资源一共只有5份,所以信号量n也为5.
假设1先申请信号量,但运气不好执行完第一步,就被切走了,然后2,3,4,5,6都正常申请了信号量,此时信号量已经为0,后面7到10的进程都不能再申请了。
但此时1号又开始继续执行,由于第一次进来时n是5,继续执行第2,3步,n--为4,然后再写回到内存,此时n从0变成了4,这不就差了吗,明明都没有资源了,结果信号量成了4,后面的进程又可以继续申请,这样肯定就出错了。
所以n--因为时序问题导致n有中间状态,可能导致数据不一致、但如果n只有一行汇编,那么该操作就是原子的!
所以4.原子性:要么不做,要么做完,没有中间状态,就称为原子性。
信号量的概念及使用
概念:
System V信号量是一种在操作系统中提供的进程间通信(IPC)机制,用于实现进程之间的同步和互斥。它通过对计数器进行操作来控制资源的访问。
System V信号量由一个整型的标识符(semaphore identifier)来标识,每个标识符对应着一个信号量集合(semaphore set)。信号量集合中可以包含多个单独的信号量,每个信号量都有一个非负整数值。
使用:
System V信号量提供了以下三个基本操作:
创建信号量集合:使用
semget()
系统调用创建一个新的信号量集合。该调用返回一个信号量标识符,用于后续的信号量操作。控制信号量集合:使用
semctl()
系统调用控制信号量集合。通过该调用,可以设置信号量的初始值、获取或改变信号量的值,以及删除信号量集合等。操作信号量:使用
semop()
系统调用来操作信号量。该调用接受一个信号量标识符和一组操作,可以通过操作来获取或释放信号量,并进行其他的控制操作。
semop()
系统调用所接受的操作包括:
- P(等待)操作:通过对信号量的值进行减一操作(如果信号量值大于零),或者使得调用进程进入等待状态(如果信号量值为零)。
- V(释放)操作:通过对信号量的值进行加一操作,并唤醒等待该信号量的其他进程。
- 通过适当的组合和操作,可以实现对资源的互斥访问和并发控制。多个进程可以通过对信号量的操作来协调彼此之间的动作,以保证数据的一致性和正确性。
当介绍System V信号量相关函数时,常用的函数包括
semget()
、semctl()
和semop()
。下面将分别介绍它们的参数及用法:
semget()
函数:int semget(key_t key, int nsems, int semflg);
- 参数:
key
:唯一key值,用于标识要创建或获取的信号量集合。nsems
:指定信号量集合中信号量的数量。semflg
:标志参数,用于指定信号量的创建方式和访问权限。- 用法:
- 通过指定一个键值和其他参数,调用
semget()
函数可以创建一个新的信号量集合,或者获取一个已经存在的信号量集合。- 返回值是一个信号量标识符(semaphore identifier),它用于后续对信号量集合的控制和操作。
semctl()
函数:int semctl(int semid, int semnum, int cmd, ...);
- 参数:
semid
:信号量标识符,用于指定要操作的信号量集合。semnum
:指定具体的信号量在集合中的索引,用于标识要操作的信号量。cmd
:执行的控制命令,用于指定具体的操作。arg
:根据不同的命令,需要提供的参数。- 用法:
semctl()
函数用于控制和管理信号量集合。- 可以通过指定不同的控制命令(
cmd
)来实现不同的操作,例如设置信号量的初始值、获取或改变信号量的值,以及删除信号量集合等。- 具体的参数(如
arg
)根据不同的命令而有所不同。
semop()
函数:int semop(int semid, struct sembuf *sops, unsigned nsops);
- 参数:
semid
:信号量标识符,用于指定要操作的信号量集合。sops
:指向一个sembuf
结构体数组的指针,包含了一组操作。nsops
:指定操作的数量。- 用法:
semop()
函数用于对信号量进行操作。- 通过组合不同的操作,可以实现对信号量的获取、释放等操作。
sops
参数是一个指向sembuf
结构体数组的指针,sembuf
结构体定义了每个操作的具体信息,包括信号量的编号、操作类型(P操作或V操作)以及操作标志等。nsops
参数指定要执行的操作数量。其中。sembuf结构体大概如下:
sembuf
结构体是用于在System V信号量中指定操作的结构体,它包含以下字段:
unsigned short sem_num
:指定要操作的信号量在集合中的索引,从0开始计数。short sem_op
:指定要执行的操作。
- 如果
sem_op
的值大于0,则表示进行V(释放)操作,即增加信号量的值。- 如果
sem_op
的值小于0,则表示进行P(等待)操作,即减少信号量的值。- 如果
sem_op
的值等于0,则表示进行Z(零)操作,如果信号量的值为0,则等待。该操作通常用于同步操作,以等待某个特定条件的发生。short sem_flg
:用于指定操作的标志。
IPC_NOWAIT
:如果无法进行操作(例如信号量的值为0且sem_op
为负数),则立即返回,不进行等待。SEM_UNDO
:系统在进程意外终止时,会自动撤销该进程对信号量的操作,以避免死锁。