前言
对于数据库(可以看作共享内存)等高并发的服务器,需要同时应对很多请求,这些请求里有读请求也有写请求。如果每次来一个请求的时候就给数据库加一个锁,或者使用单一信号量限制单进程访问,那么系统的性能会受到极大的影响,失去多进程的优势。为了解决这个问题,可以考虑使用读写锁。
读写锁
读写锁的功能:
读写锁是指,当允许多个进程同时以读的方式访问共享资源,但是只能允许一个进程向共享资源内写入数据,同时在写入数据时不能有进程在读数据。
总结一下,读写锁需要满足以下功能:
(1)可以允许多个进程读数据。
(2)只能有一个进程写数据。
(3)写数据的时候不能有进程读数据。
其实很好理解,因为读数据不会对数据本身造成影响,因此多个进程间不会出现冲突。而如果多个进程写数据可能会导致内容相互覆盖。同时写和读也可能得到混乱的数据。
读写锁的结构:
读写锁内部由两个子锁构成,分别为共享锁和独占锁。
在独占锁存在的前提下,读进程会获取共享锁并访问共享内存。与之不同的是,写进程在执行写动作时,会检查独占锁是否存在。若存在,他会获取独占锁,之后所有的读进程和写进程都无法访问共享内存。此时读进程可能还没完成读操作,因此写进程需要等待所有读进程离开后才开始写入。
请看下面的组图:
这里有一个歧义之处:读进程获取共享锁是指对共享锁记录的值加一,而写进程获取独占锁是将独占锁由1变为0。因此实际上共享锁就是一个计数器,表示目前访问共享内存的读进程的个数。而独占锁相当于一个状态位,表示现在共享内存内是否有写进程。之前讲过使用普通的变量在多进程的情况下会有冲突的情况,因此为了保证安全,这里需要使用信号量,而且需要使用两个信号量。
代码解析与实现:
接下来结合代码讲解:
1、关键结构声明与初始化:
typedef struct rwlock {
int unique;
int shared;
} rwlock;
union semun {
int val; // SETVAL用的值
struct semid_ds *buf; // IPC_STAT, IPC_SET用的缓冲区
unsigned short *array; // GETALL, SETALL用的数组
}; // 用于追加参数的一个结构体,必须由用户定义
int rwlock_init(rwlock* rw) {
rw->unique = semget(1, 1, 0666 | IPC_CREAT | IPC_EXCL);
rw->shared = semget(2, 1, 0666 | IPC_CREAT | IPC_EXCL);
if(rw->unique == -1 && rw->shared == -1) {
if(errno == EEXIST) { // 信号量已经存在
printf("rwlock has exist\n");
rw->unique = semget(1, 1, 0666);
rw->shared = semget(2, 1, 0666);
}
else { exit(0); }
}
else if(rw->unique > -1 && rw->shared > -1) { // 信号量不存在,新创建
printf("rwlock is created\n");
union semun arg;
arg.val = 1;
semctl(rw->unique, 0, SETVAL, arg);
arg.val = 0;
semctl(rw->shared, 0, SETVAL, arg);
}
else { // another error
exit(0);
}
return 0;
}
void* shm_init() {
int id = shmget(1, 128, 0666|IPC_CREAT);
return shmat(id, NULL, 0);
}
这里其实很简单,rwlock
结构里里面是两个整形,用于表示两个信号量的id
,在rwlock_init
中对信号量进行了初始化和赋值。为了防止重读赋值导致rwlock_init的逻辑较为复杂,但是结合注释仔细阅读也没有什么特别难以理解的。shm_init
用来初始化共享内存。
2、写操作的实现
// buf: 指向共享内存
// data: 写入的数据
void myWrite(rwlock* rw, char* buf, const char* data) {
struct sembuf sbf;
sbf.sem_flg = 0;
sbf.sem_num = 0;
// 独占锁被获取,进入临界区
sbf.sem_op = -1;
semop(rw->unique, &sbf, 1);
// 等待共享锁的信号量值为0
sbf.sem_op = 0;
semop(rw->shared, &sbf, 1);
/*
writing
your code
*/
// 独占锁被释放,离开临界区
sbf.sem_op = 1;
semop(rw->unique, &sbf, 1);
}
读操作没什么好说的,比较关键的是,这里涉及到一个semop
操作,当sbf.sem_op<0
时信号量减,sbf.sem_op>0
时信号量加,sbf.sem_op=0
的时候则进程会被阻塞直到信号量的值变为0。(关于信号量:linux多线(进)程编程——(9)信号量(一))
3、读操作的实现
// buf: 指向共享内存
// data: 接收数据的缓存
void myRead(rwlock* rw, char* buf, chat* data) {
struct sembuf sbf;
sbf.sem_flg = 0;
sbf.sem_num = 0;
// 获取独占锁,之后会马上释放,主要为了检查读占锁是否被写进程获取
sbf.sem_op = -1;
semop(rw->unique, &sbf, 1);
// 共享计数加一
sbf.sem_op = 1;
semop(rw->shared, &sbf, 1);
// 释放独占锁
sbf.sem_op = 1;
semop(rw->unique, &sbf, 1);
/*
reading
your code
*/
// 读完成,共享计数减一,表示读进程离开共享内存
sbf.sem_op = -1;
semop(rw->shared, &sbf, 1);
}
这里要注意的是,当读进程访问内存前,需要判断独占锁是否存在,当独占锁存在时,才可以进入。因此这里需要先获取独占锁。当独占锁为0时,证明共享内存中有写进程,需要阻塞当前读进程。
这里只提供了代码框架,具体的写入操作需要大家在我代码块留下的注释那里加入。
code sample
代码存储在我的github仓库下,LinuxMultiProcessProj
这里是一个使用案例。
1:rw.c
#ifndef __RW_H__
#define __RW_H__
#include <stdio.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int initState = 0;
typedef struct rwlock {
int unique;
int shared;
} rwlock;
union semun {
int val; // SETVAL用的值
struct semid_ds *buf; // IPC_STAT, IPC_SET用的缓冲区
unsigned short *array; // GETALL, SETALL用的数组
}; // 用于追加参数的一个结构体,必须由用户定义
int rwlock_init(rwlock* rw) {
rw->unique = semget(1, 1, 0666 | IPC_CREAT | IPC_EXCL);
rw->shared = semget(2, 1, 0666 | IPC_CREAT | IPC_EXCL);
if(rw->unique == -1 && rw->shared == -1) {
if(errno == EEXIST) {
printf("rwlock has exist\n");
rw->unique = semget(1, 1, 0666);
rw->shared = semget(2, 1, 0666);
}
else {
printf("fatal error in %s, %s, %d, rwlock init fail\n" \
, __FILE__ , __func__, __LINE__);
exit(0);
}
}
else if(rw->unique > -1 && rw->shared > -1) {
printf("rwlock is created\n");
union semun arg;
arg.val = 1;
semctl(rw->unique, 0, SETVAL, arg);
arg.val = 0;
semctl(rw->shared, 0, SETVAL, arg);
}
else {
printf("fatal error in %s, %s, %d, rwlock init fail\n" \
, __FILE__ , __func__, __LINE__);
exit(0);
}
return 0;
}
void* shm_init() {
int id = shmget(1, 128, 0666|IPC_CREAT);
return shmat(id, NULL, 0);
}
void myWrite(rwlock* rw, char* buf, const char* data) {
struct sembuf sbf;
sbf.sem_flg = 0;
sbf.sem_num = 0;
sbf.sem_op = -1;
semop(rw->unique, &sbf, 1);
int readerNum = semctl(rw->shared, 0, GETVAL);
printf("Writer : there are %d readers now\n", readerNum);
sbf.sem_op = 0;
semop(rw->shared, &sbf, 1);
printf("Writer : all readers have left, begin writing\n");
strcpy(buf, data);
sleep(3);
printf("Writer : write over\n");
sbf.sem_op = 1;
semop(rw->unique, &sbf, 1);
}
void myRead(rwlock* rw, char* buf, char* data) {
struct sembuf sbf;
sbf.sem_flg = 0;
sbf.sem_num = 0;
sbf.sem_op = -1;
semop(rw->unique, &sbf, 1);
sbf.sem_op = 1;
semop(rw->shared, &sbf, 1);
sbf.sem_op = 1;
semop(rw->unique, &sbf, 1);
printf("Reader : I am reading\n");
strcpy(data, buf);
sleep(2);
printf("Reader : read over\n");
sbf.sem_op = -1;
semop(rw->shared, &sbf, 1);
}
#endif
2:writer.c
#include "rw.h"
int main() {
printf("Writer : Writer is begin\n");
rwlock rw;
if(0 > rwlock_init(&rw) ) {
printf("Writer : rwlock init fail\n");
return 0;
}
else printf("Writer : rwlock init success\n");
char* buf = shm_init();
if(!buf) {
printf("Writer : shared memory init fail\n");
return 0;
}
else printf("Writer : shared memory init success\n");
while(1) {
myWrite(&rw, buf, "hello world!\n");
sleep(3);
}
return 0;
}
3:reader.c
#include "rw.h"
int main() {
printf("Reader : Writer is begin\n");
rwlock rw;
if( 0 > rwlock_init(&rw) ) {
printf("Reader : rwlock init fail\n");
return 0;
}
else printf("Reader : rwlock init success\n");
char* shmbuf = shm_init();
if(!shmbuf) {
printf("Reader : shared memory init fail\n");
return 0;
}
else printf("Reader : shared memory init success\n");
char data[100];
while(1) {
myRead(&rw, shmbuf, data);
printf("%s", data);
usleep(200*1000);
}
return 0;
}
小结
这里的读写锁还不完善,但是基本的功能以及思想才是这篇文章想要传达的,希望大家看完有所收获。
传送阵:
上一篇:linux多线(进)程编程——(9)信号量(二)
下一篇:Linux多线(进)程编程——(10)信号