Linux进程通信之同步

1 互斥锁

1.1 简介

  互斥锁(Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。
  互斥锁用于保护保护临界区的原子性访问。虽说互斥锁保护的是临界区,但是实际上保护的是临界区的数据。

/* Initialize a mutex.  */
int pthread_mutex_init (pthread_mutex_t *__mutex, const pthread_mutexattr_t *__mutexattr);  //初始化互斥锁
/* Destroy a mutex.  */
int pthread_mutex_destroy (pthread_mutex_t *__mutex);       //销毁一个互斥锁
/* Try locking a mutex.  */
int pthread_mutex_trylock (pthread_mutex_t *__mutex);       //尝试获得互斥锁
/* Lock a mutex.  */
int pthread_mutex_lock (pthread_mutex_t *__mutex);          //加锁
int pthread_mutex_unlock (pthread_mutex_t *__mutex)         //解锁
int pthread_mutex_attr_*                                    //mutex属性操作的一系列函数

  pthread_mutex_t是互斥锁的原型,该互斥锁的初始化方式分为两种:

  • 如果该锁为静态变量,则可初始化为PTHREAD_MUTEX_INITIALIZER
  • 普通变量需要调用pthread_mutex_init进行初始化。

  pthread_mutexattr_t是希望设置的互斥锁的属性原型。互斥锁锁的基本格式为:

pthread_mutex_lock(mutex);
//临界区代码
pthread_mutex_unlock(mutex);

  如果一个线程尝试锁住已经被锁住的锁则会阻塞直到该锁被释放。一般可以使用pthread_mutex_trylock是锁的非阻塞函数,如果该锁已经被锁则返回一个EBUSY错误。这几个函数的返回值与一般的API的返回值类似:-1表示失败,0表示成功,其他值为错误码。

1.2 生产者-消费者模型

  生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
  要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
  在多线程程序中可能存在资源竞争问题。一般是通过信号量和互斥锁来保证线程安全。

#define MAX_LEN 256
struct mutex_shared
{
    pthread_mutex_t mutex;
    int buff[MAX_LEN];
};


void producer(void *arg)
{
    struct mutex_shared *new_data = (struct mutex_shared *)(arg);
    lpthread_mutex_lock(&(new_data->mutex));
    int i = 0;
    for(;i < MAX_LEN && new_data->buff[i] != -1;i ++)
        ;
        
    if(i != MAX_LEN)
    {
        new_data->buff[i] = 1;
        printf("produce %d into buffer!\n", i);
    }
    
    lpthread_mutex_unlock(&(new_data->mutex));
}

void comsumer(void *arg)
{
    struct mutex_shared *new_data = (struct mutex_shared *)(arg);
    lpthread_mutex_lock(&(new_data->mutex));
    int i = 0;
    for(;i < MAX_LEN && new_data->buff[i] == -1;i ++)
        ;
        
    if(i != MAX_LEN)
    {
        new_data->buff[i] = -1;
        printf("consume %d into buffer!\n", i);
    }
    
    lpthread_mutex_unlock(&(new_data->mutex));
}


void pro_com_test(int argc, char **argv)
{
    const int produce_no = 10;
    const int consume_no = 10;
    pthread_t con_ths[consume_no];
    pthread_t pro_ths[produce_no];
    
    struct mutex_shared data;
    lpthread_mutex_init(&data.mutex, NULL);
    memset(data.buff, -1, MAX_LEN);
    
    //创建线程
    for(int i = 0;i < produce_no;i ++)
    {
        lpthread_create(&pro_ths[i], NULL, producer, &data);
    }
    
    for(int i = 0;i < consume_no;i ++)
    {
        lpthread_create(&con_ths[i], NULL, comsumer, &data);
    }
    
    //等待
    for(int i = 0;i < produce_no;i ++)
    {
        lpthread_join(pro_ths[i], NULL);
    }
    
    for(int i = 0;i < consume_no;i ++)
    {
        lpthread_join(con_ths[i], NULL);
    }
}

2 条件变量

2.1 简介

  互斥锁用来保证临界区的原子性访问,而信号量则用来等待阻塞线程,当线程唤醒的条件满足时则唤醒线程。

/* Initialize condition variable COND using attributes ATTR, or use
   the default values if later is NULL.  */
int pthread_cond_init (pthread_cond_t * __cond, const pthread_condattr_t * __cond_attr);
/* Destroy condition variable COND.  */
int pthread_cond_destroy (pthread_cond_t *__cond);
/* Wake up one thread waiting for condition variable COND.  */
int pthread_cond_signal (pthread_cond_t *__cond);
/* Wake up all threads waiting for condition variables COND.  */
int pthread_cond_broadcast (pthread_cond_t *__cond);
/* Wait for condition variable COND to be signaled or broadcast.
   MUTEX is assumed to be locked before.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
int pthread_cond_wait (pthread_cond_t * __cond, pthread_mutex_t * __mutex);
/* Wait for condition variable COND to be signaled or broadcast until
   ABSTIME.  MUTEX is assumed to be locked before.  ABSTIME is an
   absolute time specification; zero is the beginning of the epoch
   (00:00:00 GMT, January 1, 1970).

   This function is a cancellation point and therefore not marked with
   __THROW.  */
int pthread_cond_timedwait (pthread_cond_t * __cond, pthread_mutex_t * __mutex, const struct timespec * __abstime);
int pthread_cond_attr* //系列函数
  • pthread_cond_init:如果为静态函数则使用PTHREAD_CONDA_INITIALIZER初始化,否则用这个API初始化;
  • pthread_cond_destroy:销毁条件变量;
  • pthread_conda_signal:唤醒因为条件变量不满足而睡眠的线程;
  • pthread_conda_broadcast:广播,唤醒一组条件变量的线程;
  • pthread_conda_wait:线程睡眠等待条件变量满足;
  • pthread_cond_timewait:条件变量设置超时;
  • pthread_conda_attr*:属性相关操作函数。

2.2 互斥同步

  使用条件变量改善上面的程序。需要注意的条件变量的核心是满足条件唤醒,不满足等待,而互斥锁的核心为锁。

void cond_producer(void *arg)
{
    for(;;)
    {
        struct mutex_shared *new_data = (struct mutex_shared *)(arg);
        lpthread_mutex_lock(&(new_data->mutex));
        int i = 0;
        for(;i < MAX_LEN && new_data->buff[i] != -1;i ++)
            ;
            
        if(i != MAX_LEN)
        {
            new_data->buff[i] = 1;
            if(new_data->n_ready == 0)
            {
                lpthread_cond_signal(&new_data->cond);
            }
            
            new_data->n_ready ++;
            printf("produce %d into buffer!\n", i);
        }
        
        lpthread_mutex_unlock(&(new_data->mutex));
    }
}

void cond_comsumer(void *arg)
{
    struct mutex_shared *new_data = (struct mutex_shared *)(arg);
    lpthread_mutex_lock(&(new_data->mutex));
    int i = 0;
    for(;i < MAX_LEN && new_data->buff[i] == -1;i ++)
        ;
        
    if(i != MAX_LEN)
    {
        if(new_data->n_ready == 0)
        {
            lpthread_cond_wait(&new_data->cond, &new_data->mutex);
        }
        
        new_data->n_ready--;
        new_data->buff[i] = -1;
        printf("consume %d into buffer!\n", i);
    }
    
    lpthread_mutex_unlock(&(new_data->mutex));
}

void pro_cond_test(int argc, char **argv)
{
    const int produce_no = 10;
    const int consume_no = 10;
    pthread_t con_ths[consume_no];
    pthread_t pro_ths[produce_no];
    
    struct mutex_shared data;
    data.n_ready = 0;
    
    lpthread_mutex_init(&data.mutex, NULL);
    lpthread_cond_init(&data.cond, NULL);
    memset(data.buff, -1, MAX_LEN);
    
    //创建线程
    for(int i = 0;i < produce_no;i ++)
    {
        lpthread_create(&pro_ths[i], NULL, cond_producer, &data);
    }
    
    for(int i = 0;i < consume_no;i ++)
    {
        lpthread_create(&con_ths[i], NULL, cond_comsumer, &data);
    }
    
    //等待
    for(int i = 0;i < produce_no;i ++)
    {
        lpthread_join(pro_ths[i], NULL);
    }
    
    for(int i = 0;i < consume_no;i ++)
    {
        lpthread_join(con_ths[i], NULL);
    }
}

3 读写锁

  读写锁是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。多读者锁,push lock用于解决读写问题(readers–writers problem)。读操作可并发重入,写操作是互斥的。读写锁又称共享-独占锁,共享即多个读者读,独占即同一时刻只能允许一个锁写。读写锁通常用互斥锁、条件变量、信号量实现。
  读写锁的基本分配规则为:

  • 只要没有线程占用锁用于写,可以有任意多个线程申请读锁用于读;
  • 只有没有线程占用锁用于读或者写,才可以申请写锁用于写。

  读写锁比较适用的场景为读的操作比写的操作频繁的场景。

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);          //初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);   //销毁读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);    //读加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);    //写加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);    //解锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); //非阻塞获得读锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); //非阻塞获得写锁

  

  • pthread_rwlock_init:用于初始化读写锁变量pthread_rwlock_t,如果声明为静态变量可以使用PTHEAD_RWLOCK_INITALIZER初始化;
  • pthread_rwlock_destroy:用于销毁读写锁;
  • pthread_rwlock_rdlock:获取一个读出锁;
  • pthread_rwlock_wrlock:获取一个写锁;
  • pthread_rwlock_unlock:解锁;
  • pthread_rwlock_tryrdlock:尝试获取一个读出锁,非阻塞的;
  • pthread_rwlock_trywrlock:尝试获取一个写锁,非阻塞的。

  使用条件变量和互斥锁实现读写锁:

  pthread_rwlock_t的定义如下:

#define MAGIC_CHECK 0x19283746
typedef struct lpthread_rwlock_t
{
    pthread_mutex_t mutex;              //互斥锁
    pthread_cond_t cond_readers;        //读
    pthread_cond_t cond_writers;        //写
    int wait_readers;                      //等待读者数量
    int wait_writers;                      //等待写数量
    int refercount;                     //引用计数
    int magic;                          //用于检查当前对象是否初始化
}lpthread_rwlock_t;

typedef int lpthread_rwlockattr_t;
#define LPTHREAD_RWLOCK_INITIALIZER {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER, 0, 0, 0, MAGIC_CHECK}
int lpthread_rwlock_init(lpthread_rwlock_t * rwlock, const lpthread_rwlockattr_t * attr)
{
    if(attr != 0)
    {
        err_exit("Error Parameters", attr);
    }
    
    int ret = 0;
    if((ret = pthread_mutex_init(&rwlock->mutex, NULL)) != 0)
    {
        return ret;
    }
    
    if((ret = pthread_cond_init(&rwlock->cond_readers, NULL)) != 0)
    {
        pthread_mutex_destroy(&rwlock->mutex);
        return ret;
    }
    
    if((ret = pthread_cond_init(&rwlock->cond_writers, NULL)) != 0)
    {
        pthread_mutex_destroy(&rwlock->mutex);
        pthread_cond_destroy(&rwlock->cond_readers);
    }
    rwlock->magic = MAGIC_CHECK;
    rwlock->wait_readers = 0;
    rwlock->wait_writers = 0;
    rwlock->refercount = 0;
    return ret;
}

int lpthread_rwlock_destroy(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
    {
        return EINVAL;
    }
    
    if(rwlock->refercount != 0 || 0 != rwlock->wait_readers || 0 != rwlock->wait_writers)
    {
        return EBUSY;
    }
    
    int ret = 0;
    if((ret =  pthread_mutex_destroy(&rwlock->mutex)) != 0)
        return ret;
        
    if(( ret = pthread_cond_destroy(&rwlock->cond_readers)) != 0)
        return ret;
        
    if(( ret = pthread_cond_destroy(&rwlock->cond_writers)) != 0)
        return ret;
    
    return ret;
}

int lpthread_rwlock_rdlock(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
        return EINVAL;

    int ret = 0;
    if((ret = pthread_mutex_lock(&rwlock->mutex)) != 0)
        return ret;

    //refercount < 0 表示当前有写
    while(rwlock->refercount < 0 || rwlock->wait_writers > 0)
    {
        rwlock->wait_readers ++;
        ret = pthread_cond_wait(&rwlock->cond_readers, &rwlock->mutex);
        rwlock->wait_readers --;
        if(ret != 0)
            break;
    }

    if(ret == 0)
    {
        rwlock->refercount ++;
    }

    ret = pthread_mutex_unlock(&rwlock->mutex);
    return ret;
}

int lpthread_rwlock_wrlock(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
        return EINVAL;

    int ret = 0;
    if((ret = pthread_mutex_lock(&rwlock->mutex)) != 0)
        return ret;

    //refercount < 0 表示当前有锁占有
    while(rwlock->refercount != 0)
    {
        rwlock->wait_writers ++;
        ret = pthread_cond_wait(&rwlock->cond_writers, &rwlock->mutex);
        rwlock->wait_writers --;
        if(ret != 0)
            break;
    }

    if(ret == 0)
    {
        rwlock->refercount = -1;            //当前有一个写锁占用
    }

    ret = pthread_mutex_unlock(&rwlock->mutex);
    return ret;
}

int lpthread_rwlock_unlock(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
        return EINVAL;

    int ret = 0;
    if((ret = pthread_mutex_lock(&rwlock->mutex)) != 0)
        return ret;

    //释放锁
    if(rwlock->refercount == -1)
    {
        rwlock->refercount = 0;         //释放读锁
    }
    else if(rwlock->refercount > 0)
    {
        rwlock->refercount --;          //释放一个写锁
    }
    else
    {
        return EINVAL;
    }
    
    //条件变量通知,优先处理写锁
    if(rwlock->wait_writers > 0 && rwlock->refercount == 0)
    {
        ret = pthread_cond_signal(&rwlock->cond_writers);
    }
    else if(rwlock->wait_readers > 0)
    {
        ret = pthread_cond_broadcast(&rwlock->cond_readers);
    }

    ret = pthread_mutex_unlock(&rwlock->mutex);
    return ret;
}

int lpthread_rwlock_tryrdlock(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
    return EINVAL;

    int ret = 0;
    if((ret = pthread_mutex_lock(&rwlock->mutex)) != 0)
        return ret;

    //refercount < 0 表示当前有锁占有
    if(rwlock->refercount == -1 || rwlock->wait_writers > 0)
    {
        ret = EBUSY;
    }
    else
    {
        rwlock->refercount ++;
    }

    ret = pthread_mutex_unlock(&rwlock->mutex);
    return ret;
}

int lpthread_rwlock_trywrlock(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
        return EINVAL;

    int ret = 0;
    if((ret = pthread_mutex_lock(&rwlock->mutex)) != 0)
        return ret;

    //refercount < 0 表示当前有锁占有
    if(rwlock->refercount != 0)
    {
        ret = EBUSY;
    }
    else
    {
        rwlock->refercount = -1;
    }

    ret = pthread_mutex_unlock(&rwlock->mutex);
    return ret;
}

  上述代码有个问题就是,如果在条件变量等待时有函数调用了pthread_cancel或者pthread_exit取消了线程,而该部分代码正执行到*cond_wait处,则读写锁的值就是不正确的,而且互斥锁也未得到有效的释放。有效的解决方案是添加两个后处理函数:

void lpthread_rwlock_cancel_rdwait(void *arg)
{
    lpthread_rwlock_t *rw = arg;
    rw->wait_readers --;
    pthread_mutex_unlock(&rw->mutex);
}

void lpthread_rwlock_cancel_wrwait(void *arg)
{
    lpthread_rwlock_t *rw = arg;
    rw->wait_writers --;
    pthread_mutex_unlock(&rw->mutex);
}

  使用下面两个线程后处理函数进行后处理。并且获取读写锁的函数修改为:

int lpthread_rwlock_rdlock(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
        return EINVAL;

    int ret = 0;
    if((ret = pthread_mutex_lock(&rwlock->mutex)) != 0)
        return ret;

    //refercount < 0 表示当前有写
    while(rwlock->refercount < 0 || rwlock->wait_writers > 0)
    {
        rwlock->wait_readers ++;
        pthread_cleanup_push(lpthread_rwlock_cancel_rdwait, (void*)rwlock);     //++
        ret = pthread_cond_wait(&rwlock->cond_readers, &rwlock->mutex);
        pthread_cleanup_pop(0);                                                 //++
        rwlock->wait_readers --;
        if(ret != 0)
            break;
    }

    if(ret == 0)
    {
        rwlock->refercount ++;
    }

    ret = pthread_mutex_unlock(&rwlock->mutex);
    return ret;
}
int lpthread_rwlock_wrlock(lpthread_rwlock_t *rwlock)
{
    if(rwlock->magic != MAGIC_CHECK)
        return EINVAL;

    int ret = 0;
    if((ret = pthread_mutex_lock(&rwlock->mutex)) != 0)
        return ret;

    //refercount < 0 表示当前有锁占有
    while(rwlock->refercount != 0)
    {
        rwlock->wait_writers ++;
        pthread_cleanup_push(lpthread_rwlock_cancel_wrwait, (void*)rwlock);     //++
        ret = pthread_cond_wait(&rwlock->cond_writers, &rwlock->mutex);
        pthread_cleanup_pop(0);                                                 //++
        rwlock->wait_writers --;
        if(ret != 0)
            break;
    }

    if(ret == 0)
    {
        rwlock->refercount = -1;            //当前有一个写锁占用
    }

    ret = pthread_mutex_unlock(&rwlock->mutex);
    return ret;
}

4 记录锁

  上面提到的互斥锁,读写锁很明显的一个特点是只能作用于同一进程中的不同线程之间的资源竞争,如果是多个进程则无能为力。比如如果当前进程希望读某个文件,但是另一个进程正在写文件就会造成读写冲突,不一致的现象。

4.1 文件锁和记录锁

  文件锁或者记录锁本身就是由内核维护,保证不同进程之间也能够实现有效的资源同步或者竞争访问。
  文件上锁,顾名思义,即对整个文件进行上锁来保护文件的独占性-共享性。记录上锁,即对文件中的一部分内容进行上锁,只对该部分内容保护其独占性-共享性。文件上锁某种程度上就是记录上锁对整个文件的所有内容进行加锁的一种特例。简单举个例子就是:对于一个长为1024的文件,文件加锁就是对[0, 1023]全部范围加锁,而记录上锁就是对其中的子集[left, right]进行加锁(left和right可以自行指定)。
  文件上锁和记录上锁的优缺点相比很明显:记录上锁可以保证当进程访问文件的不同内容时,依然保证共享独占性;而文件上锁一旦一个文件占有文件,即便是当前进程不曾浸染的部分也是独占的。
  记录加锁的API为:

int fcntl(int fd, int cmd, ... /* arg */ );

struct flock {
    ...
    short l_type;    /* Type of lock: F_RDLCK,
                        F_WRLCK, F_UNLCK */
    short l_whence;  /* How to interpret l_start:
                        SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start;   /* Starting offset for lock */
    off_t l_len;     /* Number of bytes to lock */
    pid_t l_pid;     /* PID of process blocking our lock
                        (set by F_GETLK and F_OFD_GETLK) */
    ...
};

  fcntl我们一般用来修改文件的属性,也可以用来加锁。
  上述flock结构体不同成员的含义为:

  • l_type:当前所的类型:
    • F_RDLCK:读锁;
    • F_WRLCK:写锁;
    • F_UNLCK:未加锁;
  • l_whence:如何解释参数l_start的值:
    • SEEK_SET:相对于文件开头;
    • SEEK_CUR:相对于当前读写位置;
    • SEEK_END:相对于文件末尾;
  • l_start:加锁的起始地址的偏移量,和l_whence一起解释;
  • l_en:加锁的记录长度;
  • l_pid:对当前文件加锁的pid,当获取锁信息时设置,不需要用户指定。

  fcntlAPI参数:

  • fd:文件描述符;
  • cmd:对于加锁该命令有三个取值:
    • F_SETLK:加锁或者释放锁,根据参数arg的成员操作,当前进程是非阻塞的,如果无法获得锁则返回EACCESEAGAIN
    • F_SETLKW:相对于F_SETLK是阻塞的;
    • F_GETLK:检查arg指定的锁确定是否存在已经存在一个指定的锁。

  需要注意的是发出F_GETLK之后紧接着发出F_SETLK的操作并不是原子的,因此无法因此来保证有效的无阻塞的获得锁。另外记录锁和读写锁类似是独占-共享的,即读共享,写独占。并且当进程关闭文件所有描述符或者进程本身终止时,所有关联的锁被删除,记录锁无法通过fork继承。
  示例如下:

int lfcntl_lock(int fd, int cmd, int type, off_t start, int where, off_t len)
{
    struct flock arg;

    arg.l_len = len;
    arg.l_start = start;
    arg.l_type = type;
    arg.l_whence = where;
    return fcntl(fd, cmd, &arg);
}

pid_t lfcntl_lockable(int fd, int type, off_t start, int where, off_t len)
{
    struct flock arg;

    arg.l_len = len;
    arg.l_start = start;
    arg.l_type = type;
    arg.l_whence = where;

    int ret = fcntl(fd, F_GETLK, &arg);
    if(ret == -1)
        return ret;

    if(arg.l_type == F_UNLCK)
        return 0;

    return ret;
}

#define lfcntl_rd_lock(fd, offset, where, len) lfcntl_lock(fd, F_SETLK, F_RDLCK, offset, where, len)
#define lfcntl_rd_lockw(fd, offset, where, len) lfcntl_lock(fd, F_SETLKW, F_RDLCK, offset, where, len)
#define lfcntl_wr_lock(fd, offset, where, len) lfcntl_lock(fd, F_SETLK, F_WRLCK, offset, where, len)
#define lfcntl_wr_lockw(fd, offset, where, len) lfcntl_lock(fd, F_SETLKW, F_WRLCK, offset, where, len)
#define lfcntl_unlock(fd, offset, where, len) lfcntl_lock(fd, F_SETLK, F_UNLCK, offset, where, len)

//如果未上锁则返回0,上锁了则返回进程id
#define lfcntl_rd_lockable(fd, offset, where, len) lfcntl_lockable(fd, F_RDLCK, offset, where, len)
#define lfcntl_wr_lockable(fd, offset, where, len) lfcntl_lockable(fd, F_WRLCK, offset, where, len)

void lock_file(int fd, int flag)
{
    if(!flag) return;
    lfcntl_wr_lockw(fd, 0, SEEK_SET, 0);
}

void unlock_file(int fd, int flag)
{
    if(!flag) return;
    lfcntl_unlock(fd, 0, SEEK_SET, 0);
}

void fcntl_test()
{
    char file[] = "/home/grayondream/altas/ipc/build/tmp";
    pid_t pid = getpid();
    int fd = open(file, O_RDWR, FILE_MODE);
    char line[MAX_LEN] = {0};
    int n = 0;
    int flag = 0;
    for(int i = 0;i < 20;i ++)
    {
        lock_file(fd, flag);
        lseek(fd, 0, SEEK_SET);
        int len = lread(fd, line, MAX_LEN);
        line[len] = '\0';
        len = sscanf(line, "%d\n", &n);
        printf("pid = %d, no = %d\n", pid, n);
        n++;

        sprintf(line, "%d\n", n);
        lseek(fd, 0, SEEK_SET);
        lwrite(fd, line, MAX_LEN);

        unlock_file(fd, flag);
    }

}

  上面的示例为从文件中读取一个数字然后将数字加一再存入文件,flag为标志位,表示是否加锁。运行程序之间需要创建一个文件并写入一个数字,这里为1。未加锁的情况下面也看到了出现了读写不一致的情况。

➜  build git:(master) ✗ ./main & ./main
[1] 13273
pid = 13273, no = 1
pid = 13274, no = 1
pid = 13274, no = 2
pid = 13274, no = 3
pid = 13274, no = 4
pid = 13274, no = 5
pid = 13274, no = 6
pid = 13274, no = 7
pid = 13274, no = 8
pid = 13274, no = 9
pid = 13274, no = 10
pid = 13274, no = 11
pid = 13274, no = 12
pid = 13274, no = 13
pid = 13274, no = 14
pid = 13274, no = 15
pid = 13274, no = 16
pid = 13274, no = 17
pid = 13273, no = 2
pid = 13274, no = 18
pid = 13274, no = 19
pid = 13273, no = 20
pid = 13274, no = 20
pid = 13273, no = 21
pid = 13273, no = 22
pid = 13273, no = 23
pid = 13273, no = 24
pid = 13273, no = 25
pid = 13273, no = 26
pid = 13273, no = 27
pid = 13273, no = 28
pid = 13273, no = 29
pid = 13273, no = 30
pid = 13273, no = 31
pid = 13273, no = 32
pid = 13273, no = 33
pid = 13273, no = 34
pid = 13273, no = 35
pid = 13273, no = 36
pid = 13273, no = 37

  将flag修改为1之后,即加锁结果如下:

➜  build git:(master) ✗ ./main & ./main
[1] 14266
pid = 14266, no = 1
pid = 14266, no = 2
pid = 14266, no = 3
pid = 14266, no = 4
pid = 14266, no = 5
pid = 14266, no = 6
pid = 14266, no = 7
pid = 14266, no = 8
pid = 14266, no = 9
pid = 14266, no = 10
pid = 14266, no = 11
pid = 14266, no = 12
pid = 14266, no = 13
pid = 14266, no = 14
pid = 14266, no = 15
pid = 14266, no = 16
pid = 14266, no = 17
pid = 14266, no = 18
pid = 14266, no = 19
pid = 14266, no = 20
pid = 14267, no = 21
[1]  + 14266 done       ./main
pid = 14267, no = 22
pid = 14267, no = 23
pid = 14267, no = 24
pid = 14267, no = 25
pid = 14267, no = 26
pid = 14267, no = 27
pid = 14267, no = 28
pid = 14267, no = 29
pid = 14267, no = 30
pid = 14267, no = 31
pid = 14267, no = 32
pid = 14267, no = 33
pid = 14267, no = 34
pid = 14267, no = 35
pid = 14267, no = 36
pid = 14267, no = 37
pid = 14267, no = 38
pid = 14267, no = 39
pid = 14267, no = 40

4.2 劝告型上锁(建议锁)

  POSIX记录上锁也称劝告性上锁。共含义是内核维护着已由各个进程上锁的所有文件的正确信息,它不能防止一个进程写由另一个进程读锁定的某个文件,也不能防止一个进程读已由另一个进程写锁定的文件。一个进程能够无视劝告性锁而写一个读锁文件,或读一个写锁文件,前提时该进程有读或写该文件的足够权限。
  劝告性上锁对于协作进程足够。如网络编程中的协程,这些程序访问如序列号文件的共享资源,而且都在系统管理员的控制下,只要包含该序列号的真正文件不是任何进程都可写,那么在该文件被锁住期间,不理会劝告性锁的进程随意进程无法访问写它。
  每个使用文件的进程都要主动检查该文件是否有锁存在,当然都是通过具体锁的API,比如fctl记录锁F_GETTLK来主动检查是否有锁存在。如果有锁存在并被排斥,那么就主动保证不再进行接下来的IO操作。如果每一个进程都主动进行检查,并主动保证,那么就说这些进程都以一致性的方法处理锁,(这里的一致性方法就是之前说的两个主动)。但是这种一致性方法依赖于编写进程程序员的素质,也许有的程序员编写的进程程序遵守这个一致性方法,有的不遵守。不遵守的程序员编写的进程程序会怎么做呢?也许会不主动判断这个文件有没有加上文件锁或记录锁,就直接对这个文件进行IO操作。此时这种有破坏性的IO操作会不会成功呢?如果是在建议性锁的机制下,这种破坏性的IO就会成功。因为锁只是建议性存在的,并不强制执行。内核和系统总体上都坚持不使用建议性锁机制,它们依靠程序员遵守这个规定。(Linux默认锁为建议锁)

4.3 强制型上锁

  对于强制性上锁,内核检查每个readwrite请求,以验证其操作不会干扰由某个进程持有的某个锁,对于通常的时阻塞描述符,与某个强制性锁冲突的read和write将把调用进程投入睡眠,直到该锁释放为止,对于非阻塞描述符,与某个强制锁冲突的readwrite将导致他们返回一个EAGAIN
  为对某个特定的文件实行强制锁应满足:

  • 组成员执行位必须关掉;
  • SGID位必须打开。

4.4 读写锁的优先级问题

  读写优先级需要面对两个问题:

  • 当资源已经被读锁占用,并且有一个写锁正在等待时,是否允许另一个读锁?这可能导致写饥饿;
  • 等待着的写和读的优先级如何?

  下面程序是对第一个问题的回答,下面的程序通过sleep交叉不同进程对锁的索取:

void rd_wr_test()
{
    char file[] = "./1";
    int fd = open(file, O_RDWR, FILE_MODE);
    printf("%s : 父进程尝试拥有1个读锁\n", lget_time());
    lfcntl_rd_lockw(fd, 0, SEEK_SET, 0);
    printf("%s : 父进程拥有1个读锁\n", lget_time());
    pid_t pid1;
    pid_t pid2 = lfork();
    if(pid2 == 0)
    {
        sleep(1);
        printf("%s : 子进程1尝试拥有1个写锁\n", lget_time());
        lfcntl_wr_lockw(fd, 0, SEEK_SET, 0);
        printf("%s : 子进程1拥有1个写锁\n", lget_time());
        sleep(2);
        printf("%s : 子进程1释放了1个写锁\n", lget_time());
        lfcntl_unlock(fd, 0, SEEK_SET, 0);
        _exit(0);
    }

    pid1 = fork();
    if(pid1 == 0)
    {
        sleep(3);
        printf("%s : 子进程2尝试拥有1个读锁\n", lget_time());
        lfcntl_rd_lockw(fd, 0, SEEK_SET, 0);
        printf("%s : 子进程2拥有1个读锁\n", lget_time());
        sleep(4);
        printf("%s : 子进程2释放了1个读锁\n", lget_time());
        lfcntl_unlock(fd, 0, SEEK_SET, 0);
        _exit(0);
    }

    sleep(5);
    printf("%s : 父进程释放了1个读锁\n", lget_time());
    lfcntl_unlock(fd, 0, SEEK_SET, 0);
    waitpid(pid1, 0, 0);
    waitpid(pid2, 0, 0);
    return;
}

  执行结果中可以看到是允许的。

20:28:16.176229 : 父进程尝试拥有1个读锁
20:28:16.176444 : 父进程拥有1个读锁
20:28:17.176819 : 子进程1尝试拥有1个写锁
20:28:19.176926 : 子进程2尝试拥有1个读锁
20:28:19.177178 : 子进程2拥有1个读锁
20:28:21.176859 : 父进程释放了1个读锁
20:28:23.177314 : 子进程2释放了1个读锁
20:28:23.177412 : 子进程1拥有1个写锁
20:28:25.177590 : 子进程1释放了1个写锁

  下面过程通过父进程拥有写锁再释放保证独占行,保证之后的两个读写锁同时竞争。

void rd_wr_test2()
{
    char file[] = "./1";
    int fd = open(file, O_RDWR, FILE_MODE);
    printf("%s : 父进程尝试拥有1个写锁\n", lget_time());
    lfcntl_wr_lockw(fd, 0, SEEK_SET, 0);
    printf("%s : 父进程拥有1个写锁\n", lget_time());
    pid_t pid1;
    pid_t pid2 = lfork();
    if(pid2 == 0)
    {
        sleep(0);
        printf("%s : 子进程1尝试拥有1个写锁\n", lget_time());
        lfcntl_wr_lockw(fd, 0, SEEK_SET, 0);
        printf("%s : 子进程1拥有1个写锁\n", lget_time());
        sleep(1);
        printf("%s : 子进程1释放了1个写锁\n", lget_time());
        lfcntl_unlock(fd, 0, SEEK_SET, 0);
        _exit(0);
    }

    pid1 = fork();
    if(pid1 == 0)
    {
        sleep(1);
        printf("%s : 子进程2尝试拥有1个读锁\n", lget_time());
        lfcntl_rd_lockw(fd, 0, SEEK_SET, 0);
        printf("%s : 子进程2拥有1个读锁\n", lget_time());
        sleep(1);
        printf("%s : 子进程2释放了1个读锁\n", lget_time());
        lfcntl_unlock(fd, 0, SEEK_SET, 0);
        _exit(0);
    }

    sleep(3);
    printf("%s : 父进程释放了1个写锁\n", lget_time());
    lfcntl_unlock(fd, 0, SEEK_SET, 0);
    waitpid(pid1, 0, 0);
    waitpid(pid2, 0, 0);
    return;
}

  测试分为两组,通过调整两个进程中sleep等待的时间改变读写锁的请求顺序,结果分别如下,第一个为写请求先于读,第二个相反.可以看到读写请求的处理顺序是按照FIFO顺序进行处理的。

➜  build git:(master) ✗ ./main 
20:45:09.636658 : 父进程尝试拥有1个写锁
20:45:09.636880 : 父进程拥有1个写锁
20:45:09.637212 : 子进程1尝试拥有1个写锁
20:45:10.637416 : 子进程2尝试拥有1个读锁
20:45:12.637240 : 父进程释放了1个写锁
20:45:12.637360 : 子进程1拥有1个写锁
20:45:13.637479 : 子进程1释放了1个写锁
20:45:13.637846 : 子进程2拥有1个读锁
20:45:14.638017 : 子进程2释放了1个读锁
➜  build git:(master) ✗ ./main
20:41:20.163030 : 父进程尝试拥有1个写锁
20:41:20.163304 : 父进程拥有1个写锁
20:41:21.163718 : 子进程1尝试拥有1个写锁
20:41:23.163771 : 子进程2尝试拥有1个读锁
20:41:25.163718 : 父进程释放了1个写锁
20:41:25.163818 : 子进程2拥有1个读锁
20:41:29.163925 : 子进程2释放了1个读锁
20:41:29.164005 : 子进程1拥有1个写锁
20:41:31.164137 : 子进程1释放了1个写锁

4.5 记录锁的其他用途

  可以使用记录锁保证进程在系统中只存在一个副本,即为每个进程关联一个独一无二的文件。
  文件锁的实现也可以使用绝对路径下的文件的唯一性实现,当然这样存在一些其他问题:

  • 如果进程意外结束,文件无法有效删除,可能出现问题;
  • 使用文件进行唯一性确认本身就是轮询效率堪忧。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值