在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。所谓进程同步(线程同步同理),主要是解决临界资源互斥访问的问题。如多个进程访问同一片共享内存,这片共享内存必须互斥使用。

一.进程同步

在Linux下,进程同步的解决方式主要有四种:
1. 信号量
2. 文件锁
3. 无锁CAS
4. 校验方式(CRC32校验)

1. 信号量

可以借助Linux的信号量的PV操作来实现对临界资源的互斥访问。
详情可看博文:
http://blog.csdn.net/okiwilldoit/article/details/78401536

2. 文件锁

linux下可以使用flock()函数对文件进行加锁解锁等操作。简单介绍下flock()函数:
1. 定义函数 int flock(int fd,int operation);
2. 函数说明 flock()会依参数operation所指定的方式对参数fd所指的文件做各种锁定或解除锁定的动作。此函数只能锁定整个文件,无法锁定文件的某一区域。
3. 参数 operation有下列四种情况:
LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。
LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。
LOCK_UN 解除文件锁定状态。
LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做OR(|)组合。
4. 返回值 返回0表示成功,若有错误则返回-1,错误代码存于errno。

注意:单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。

flock锁的释放非常具有特色,即可调用LOCK_UN参数来释放文件锁,也可以通过关闭fd的方式来释放文件锁(flock的第一个参数是fd),意味着flock会随着进程的关闭而被自动释放掉。

简而言之:
一个进程加LOCK_SH,其他进程也可以加LOCK_SH,但不能加LOCK_EX锁。
一个进程加LOCK_EX,其他进程不能对该文件加任何锁。
这种机制类似于读写锁,LOCK_SH是读锁,LOCK_EX是写锁。

下面是一个实例:
进程1:

#include <stdio.h>
#include <sys/file.h>
#include <unistd.h>
#include <errno.h>
int main(void)  
{  
    FILE *fp = NULL;   
    if ((fp = fopen("./file_lock", "wb+")) == NULL) //打开文件  
    {
        printf("file open error,errno=%d!\n",errno);
        return -1;
    }       
    if (flock(fp->_fileno, LOCK_EX) != 0) //给该文件加互斥锁
        printf("file lock by others\n");//加锁失败,阻塞
    while(1)
    {     
        printf("process1-ex\n");
        sleep(1);  
    }    
    flock(fp->_fileno, LOCK_UN); //文件解锁 
    fclose(fp); //关闭文件       
    return 0;  
}

进程2:

#include <stdio.h>
#include <sys/file.h>
#include <unistd.h>
#include <errno.h>
int main(void)  
{  
    FILE *fp = NULL;  
    int i = 0;  
    if ((fp = fopen("./file_lock", "wb+")) == NULL) //打开文件
    {
        printf("file open error,errno=%d!\n",errno);
        return -1;
    }   
    if(flock(fp->_fileno, LOCK_SH) != 0)//文件加共享锁
    {
        printf("file lock by others\n");//加锁失败,阻塞
    } 
    while(1) //进入循环  
    {     
        printf("process2-sh\n");
        sleep(1);  
    }     
    flock(fp->_fileno, LOCK_UN); //释放文件锁  
    fclose(fp); //关闭文件
    return 0;  
}

进程1运行时,其他进程无法获得任何锁运行;
进程2运行时,其他进程可以获得共享锁运行;

3.无锁CAS访问

上面的加锁的方法,虽然可以保证数据的一致性,但是加锁会引起性能的下降,多个进程竞争同一个锁,抢占失败后强制上下文切换。还有一种方法就是使用原子指令。有一个重要的方法叫做CAS(compare and swap)。
CAS是一组原语指令,用来实现多进/线程下的变量同步。
在 x86下的指令CMPXCHG实现了CAS,前置LOCK既可以达到原子性操作。
CAS原语有三个参数,内存地址,期望值,新值。如果内存地址的值==期望值,表示该值未修改,此时可以修改成新值。否则表示修改失败,返回false,由用户决定后续操作。

bool CAS(T* addr, T expected, T newValue) 
{ 
      if( *addr == expected ) 
     { 
          *addr =  newValue; 
           return true; 
     } 
     return false; 
 }

如何使用CAS实现无锁呢?

例如有多个进程访问共享内存中的某个变量,从内存中读取该变量的值为expected,你想要更新该变量的值为newValue,可以调用CAS函数,更新共享内存中的值。因为这是一组原子操作,所以在更新的过程中,不会有其他进程/线程访问到这个变量。

GCC的CAS,GCC4.1+版本中支持CAS的原子操作(完整的原子操作可参看 GCC Atomic Builtins)

bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)

  • 1
  • 2

4. 校验方式

可以用crc32校验的方式,把变量a的crc32值记录下来,存入另外的变量b。
在写入变量a的时候,更新完变量a后,再计算出a的crc32值,更新变量b;
在读取变量a的时候,把读出的值a的crc32值和另外的变量b进行比较,如果不相同,就说明变量a正在更新中,从而实现对该变量的无锁互斥访问。

这种方式虽然效率比不上CAS,但相对CAS来说,比较简单可控。

二.线程同步

同一进程的不同线程可以共享全局变量,所以线程间互斥访问全局变量,也需要同步手段。主要有五种方式:
1. 互斥锁(pthread_mutex_t )
2. 条件变量(需要与互斥锁结合使用)
3. 信号量
4. 无锁CAS
5. 校验方式(CRC32校验)

与进程同步类似,信号量和无锁CAS也支持线程同步,其他方式还是互斥锁和条件信号量。