Linux多线(进)程编程——番外2:信号量实现读写锁

前言

对于数据库(可以看作共享内存)等高并发的服务器,需要同时应对很多请求,这些请求里有读请求也有写请求。如果每次来一个请求的时候就给数据库加一个锁,或者使用单一信号量限制单进程访问,那么系统的性能会受到极大的影响,失去多进程的优势。为了解决这个问题,可以考虑使用读写锁。

读写锁

读写锁的功能
读写锁是指,当允许多个进程同时以读的方式访问共享资源,但是只能允许一个进程向共享资源内写入数据,同时在写入数据时不能有进程在读数据。
总结一下,读写锁需要满足以下功能:
(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)信号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值