博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

http://segmentfault.com/a/1190000000630435

http://blog.csdn.net/luansxx/article/details/7736618

 

这里的“不相干”,定义为:

  • 这几个进程没有父子关系,也没有 Server/Client 关系
  • 这一片共享内存一开始不存在,第一个要访问它的进程负责新建
  • 也没有额外的 daemon 进程能管理这事情

看上去这是一个很简单的问题,实际上不简单。有两大问题:

进程在持有互斥锁的时候异常退出

如果用传统 IPC 的 semget 那套接口,是没法解决的。实测发现,down 了以后进程退出,信号量的数值依然保持不变。

用 pthread (2013年的)新特性可以解决。在创建 pthread mutex 的时候,指定为 ROBUST 模式。

pthread_mutexattr_t ma;

pthread_mutexattr_init(&ma);
pthread_mutexattr_setpshared(&ma, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_setrobust(&ma, PTHREAD_MUTEX_ROBUST);

pthread_mutex_init(&c->lock, &ma);

注意,pthread 是可以用于多进程的。指定 PTHREAD_PROCESS_SHARED 即可。

关于 ROBUST,官方解释在:

http://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_mutexattr_setrobust.html

需要注意的地方是:

如果持有 mutex 的线程退出,另外一个线程在 pthread_mutex_lock 的时候会返回 EOWNERDEAD。这时候你需要调用 pthread_mutex_consistent 函数来清除这种状态,否则后果自负。

写成代码就是这样子:

int r = pthread_mutex_lock(lock);
if (r == EOWNERDEAD)
  pthread_mutex_consistent(lock);

所以要使用这个新特新的话,需要比较新的 GCC ,要 2013 年以后的版本。

好了第一个问题解决了。我们可以在初始化共享内存的时候,新建一个这样的 pthread mutex。但是问题又来了:

怎样用原子操作新建并初始化这一片共享内存?

这个问题看上去简单至极,不过如果用这样子的代码:

void *p = get_shared_mem();
if (p == NULL)
    p = create_shared_mem_and_init_mutex();
lock_shared_mem(p);
....

是不严谨的。如果共享内存初始化成全 0,那可能碰巧还可以。但我们的 mutex 也是放到共享内存里面的,是需要 init 的。

想象一下四个进程同时执行这段代码,很可能某两个进程发现共享内存不存在,然后同时新建并初始化信号量。某一个 lock 了 mutex,然后另外一个又 init mutex,就乱了。

可见,在 init mutex 之前,我们就已经需要 mutex 了。问题是,哪来这样的 mutex?前面已经说了传统 IPC 没法解决第一个问题,所以也不能用它。

其实,Linux 的文件系统本身就有这样的功能。

首先 shm_open 那一系列的函数是和文件系统关联上的。

~ ll /dev/shm/

其实 /dev/shm 是一个 mount 了的文件系统。这里面放的就是一堆通过 shm_open 新建的共享内存。都是以文件的形式展现出来。可以 rm,rename,link 各种文件操作。

其实 link 函数,也就是硬链接。是完成“原子操作”的关键所在。

搞过汇编的可能知道 CMPXCHG 这类(两个数比较,符合条件则交换)指令,是原子操作内存的最底层指令,最底层的信号量是通过它实现的。

而 link 系统调用,类似的,是系统调用级,原子操作文件的最底层指令。处于 link 操作中的进程即便被 kill 掉,在内核中也会完成最后一次这次系统调用,对文件不会有影响,不存在 “link 了一半” 这种状态,它是“原子”的。

伪代码如下:

shm_open("ourshm_tmp", ...);
// ... 初始化 ourshm_tmp 副本 ...

if (link("/dev/shm/ourshm_tmp", "/dev/shm/ourshm") == 0) {
   // 我成功创建了这片共享内存
} else {
   // 别人已经创建了
}
shm_unlink("ourshm_tmp");

首先新建初始化一份副本。然后用 link 函数。

最后无论如何都要 unlink 掉副本。

开源项目 kbz-event

这两种方法,貌似在各类经典书籍中都没提及,因为是 2013 年新出的,也是因为 Unix 鼓励用管道进行这类通信的原因。

在同类开源项目中。D-Bus 用的是另外的 daemon 进程去管理 socket。Android 的 IPC 则用了另外的内核模块(netlink 接口)来完成。

总之,都是用了额外的接口。

因此我开发了不需要额外 daemon 的轻量级 IPC 通信框架 kbz-event

欢迎各种围观!

===============================================================

进程间共享数据的保护,需要进程互斥锁。与线程锁不同,进程锁并没有直接的C库支持,但是在Linux平台,要实现进程之间互斥锁,方法有很多,大家不妨回忆一下你所了解的。下面就是标准C库提供的一系列方案。

1、实现方案

不出意外的话,大家首先想到的应该是信号量(Semaphores)。对信号量的操作函数有两套,一套是Posix标准,另一套是System V标准。

Posix信号量

[cpp] view plaincopy
 
  1. sem_t *sem_open(const char *name, int oflag);  
  2. sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);  
  3. int sem_init(sem_t *sem, int pshared, unsigned int value);  
  4. int sem_wait(sem_t *sem);  
  5. int sem_trywait(sem_t *sem);  
  6. int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);  
  7. int sem_close(sem_t *sem);  
  8. int sem_destroy(sem_t *sem);  
  9. int sem_unlink(const char *name);  

System V信号量

[cpp] view plaincopy
 
  1. int semget(key_t key, int nsems, int semflg);  
  2. int semctl(int semid, int semnum, int cmd, ...);  
  3. int semop(int semid, struct sembuf *sops, unsigned nsops);  
  4. int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);  

线程锁共享

其实还有另外一个方案:线程锁共享。这是什么呢,我估计了解它的人不多,如果你知道的话,那可以称为Linux开发牛人了。

线程锁就是pthread那一套C函数了:

[html] view plaincopy
 
  1. int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);  
  2. int pthread_mutex_destroy (pthread_mutex_t *mutex);  
  3. int pthread_mutex_trylock (pthread_mutex_t *mutex);  
  4. int pthread_mutex_lock (pthread_mutex_t *mutex);  
  5. int pthread_mutex_timedlock (pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);  
  6. int pthread_mutex_unlock (pthread_mutex_t *mutex);  

但是这只能用在一个进程内的多个线程实现互斥,怎么应用到多进程场合呢,被多个进程共享呢?

很简单,首先需要设置互斥锁的进程间共享属性:

[html] view plaincopy
 
  1. int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared);   
  2. pthread_mutexattr_t mattr;   
  3. pthread_mutexattr_init(&mattr);   
  4. pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);   

其次,为了达到多进程共享的需要,互斥锁对象需要创建在共享内存中。

最后,需要注意的是,并不是所有Linux系统都支持这个特性,程序里需要检查是否定义了_POSIX_SHARED_MEMORY_OBJECTS宏,只有定义了,才能用这种方式实现进程间互斥锁。

2、平台兼容性

我们来看看这三套方案的平台移植性。

  • 绝大部分嵌入式Linux系统,glibc或者uclibc,不支持_POSIX_SHARED_MEMORY_OBJECTS;
  • 绝大部分嵌入式Linux系统,不支持Posix标准信号量;
  • 部分平台,不支持System V标准信号量,比如Android。

3、匿名锁与命名锁

当两个(或者多个)进程没有特殊关系(比如父子进程共享)时,我们只能通过约定好的名字来访问同一个锁,这就是命名锁。然而,如果我们有其他途径定位一个锁,那么匿名锁是更好的选择。这三套方案是否都支持匿名锁与命名锁呢?

  • Posix信号量

通过sem_open创建命名锁,通过sem_init创建匿名锁,其实sem_init也支持进程内部锁。

  • System V信号量

semget中的key参数可以看成是名字,所以支持命名锁。该方案不支持匿名锁。

  • 线程锁共享

不支持命名锁,支持匿名锁。

4、缺陷

在匿名锁与命名锁的支持上,一些方案是有不足的,但这还是小问题,更严重的问题是异常状况下的死锁问题。

与多线程环境不一样的是,在多进程环境中,一个进程的异常退出不会影响其他进程,但是如果使用了进程互斥锁呢?假如一个进程获取了互斥锁,但是在访问互斥资源的代码中crash了,或者遇到信号退出了,那么其他等待同一个锁的进程(内部某个线程)就挂死了。在多线程环境中,程序异常整个进程退出,不需要考虑异常时锁的释放,多进程环境则是一个实实在在的问题。

System V信号量通过UNDO方式可以解决该问题。但是如果考虑到平台兼容性等问题,这三个方案仍不能满足需求,我会接着介绍一种更好的方案。

 

========================================================================

http://blog.csdn.net/luansxx/article/details/7737899

《进程互斥锁》中,我们看到了实现进程互斥锁的几个常见方案:Posix信号量、System V信号量以及线程锁共享,并且分析了他们的平台兼容性以及严重缺陷。这里要介绍

一种安全且平台兼容的进程互斥锁,它是基于文件记录锁实现的。

1、文件记录锁

UNIX编程的“圣经”《Unix环境高级编程》中有对文件记录锁的详细描述。

下载链接:http://dl02.topsage.com/club/computer/Unix环境高级编程.rar

记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。对于UNIX,“记录”这个定语也是误用,因为UNIX内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)。

2、平台兼容性

各种UNIX系统支持的记录锁形式: 

 

系统 建议性 强制性 fcntl lockf flock
POSIX.1 *   *    
XPG3 *   *    
SVR2 *   * *  
SVR3 SVR4 * * * *  
4.3BSD *   *   *
4.3BSDReno *   *   *

 

可以看成,记录锁在各个平台得到广泛支持。特别的,在接口上,可以统一于fcntl。

建议性锁和强制性锁之间的区别,是指其他文件操作函数(如open,read、write)是否受记录锁影响,如果是,那就是强制性的记录锁,大部分平台只是建议性的。不过,对实现进程互斥锁而言,这个影响不大。

3、接口描述

[cpp] view plaincopy
 
  1. #include <sys/types.h>  
  2. #include <unistd.h>  
  3. #include <fcnt1.h>  
  4. int fcnt1(int filedes, int cmd, .../* struct flock *flockptr */);  

对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针。

[cpp] view plaincopy
 
  1. struct flock {  
  2.     short l_type;    /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */  
  3.     short l_whence;  /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */  
  4.     off_t l_start;   /* Starting offset for lock */  
  5.     off_t l_len;     /* Number of bytes to lock */  
  6.     pid_t l_pid;     /* PID of process blocking our lock  
  7. };  

以下说明fcntl函数的三种命令:

  • F_GETLK决定由flockptr所描述的锁是否被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由flockptr所描述的锁,则这把现存的锁的信息写到flockptr指向的结构中。如果不存在这种情况,则除了将ltype设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。
  • F_SETLK设置由flockptr所描述的锁。如果试图建立一把按上述兼容性规则并不允许的锁,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。
  • F_SETLKW这是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果由于存在其他锁,那么按兼容性规则由flockptr所要求的锁不能被创建,则调用进程睡眠。如果捕捉到信号则睡眠中断。

4、实现方案

如何 基于记录锁实现进程互斥锁?

1、需要一个定义全局文件名,这个文件名只能有相关进程使用。这需要在整个系统做一个规划。

2、规定同一个进程互斥锁对应着该文件的一个字节,字节位置称为锁的编号,这样可以用一个文件实现很多互斥锁。

3、编号要有分配逻辑,文件中要记录已经分配的编号,这个逻辑也要保护,所以分配0号锁为系统锁。

4、为了实现命名锁,文件中要记录名称与编号对应关系,这个对应关系的维护也需要系统锁保护。

这些逻辑都实现在一个FileLocks类中:

[cpp] view plaincopy
 
  1. class FileLocks  
  2. {  
  3. public:  
  4.     FileLocks();  
  5.     ~FileLocks();  
  6.     size_t alloc_lock();  
  7.     size_t alloc_lock(std::string const & keyname);  
  8.     void lock(size_t pos);  
  9.     bool try_lock(size_t pos);  
  10.     void unlock(size_t pos);  
  11.     void free_lock(size_t pos);  
  12.     void free_lock(std::string const & keyname);  
  13. private:  
  14.     int             m_fd_;  
  15. };  
  16.   
  17. inline FileLocks & global_file_lock()  
  18. {  
  19.     static FileLocks g_fileblocks( "process.filelock" );  
  20.     return g_fileblocks;  
  21. }  

这里用了一个FileLocks全局单例对象,它对应的文件名是“/tmp/filelock”,在FileLocks中,分别用alloc()和alloc(keyname)分配匿名锁和命名锁,用free_lock删除锁。free_lock(pos)删除匿名锁,free_lock(keyname)删除命名锁。

对锁的使用通过lock、try_lock、unlock实现,他们都带有一个pos参数,代表锁的编号。

有了FileLocks类作为基础,要实现匿名锁和命名锁就很简单了。

4.1、匿名锁

[cpp] view plaincopy
 
  1. class FileMutex  
  2. {  
  3. public:  
  4.     FileMutex()  
  5.         : m_lockbyte_(global_file_lock().alloc_lock())  
  6.     {  
  7.     }  
  8.     ~FileMutex()  
  9.     {  
  10.         global_file_lock().free_lock(m_lockbyte_);  
  11.     }  
  12.     void lock()  
  13.     {  
  14.         global_file_lock().lock(m_lockbyte_);  
  15.     }  
  16.     bool try_lock()  
  17.     {  
  18.         return global_file_lock().try_lock(m_lockbyte_);  
  19.     }  
  20.     void unlock()  
  21.     {  
  22.         global_file_lock().unlock(m_lockbyte_);  
  23.     }  
  24. protected:  
  25.     size_t m_lockbyte_;  
  26. };  

需要注意的是,进程匿名互斥锁需要创建在共享内存上。只需要也只能某一个进程(比如创建共享内存的进程)调用构造函数,其他进程直接使用,同样析构函数也只能调用一次。

4.2、命名锁

 命名锁只需要构造函数不同,可以直接继承匿名锁实现

[cpp] view plaincopy
 
  1. class NamedFileMutex   
  2.     : public FileMutex  
  3. {  
  4. public:  
  5.     NamedFileMutex(std::string const & key)  
  6.         : m_lockbyte_(global_file_lock().alloc_lock(key))  
  7.     {  
  8.     }  
  9.     ~NamedFileMutex()  
  10.     {  
  11.         m_lockbyte_ = 0;  
  12.     }  
  13. };  

需要注意,命名锁不住析构时删除,因为可能多个对象共享该锁。

5、线程安全性