转自: https://www.cnblogs.com/my_life/articles/4538299.html
http://segmentfault.com/a/1190000000630435
http://blog.csdn.net/luansxx/article/details/7736618
这里的“不相干”,定义为:
看上去这是一个很简单的问题,实际上不简单。有两大问题:
如果用传统 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 掉副本。
这两种方法,貌似在各类经典书籍中都没提及,因为是 2013 年新出的,也是因为 Unix 鼓励用管道进行这类通信的原因。
在同类开源项目中。D-Bus 用的是另外的 daemon 进程去管理 socket。Android 的 IPC 则用了另外的内核模块(netlink 接口)来完成。
总之,都是用了额外的接口。
因此我开发了不需要额外 daemon 的轻量级 IPC 通信框架 kbz-event。
欢迎各种围观!
===============================================================
进程间共享数据的保护,需要进程互斥锁。与线程锁不同,进程锁并没有直接的C库支持,但是在Linux平台,要实现进程之间互斥锁,方法有很多,大家不妨回忆一下你所了解的。下面就是标准C库提供的一系列方案。
不出意外的话,大家首先想到的应该是信号量(Semaphores)。对信号量的操作函数有两套,一套是Posix标准,另一套是System V标准。
[cpp] view plaincopy
[cpp] view plaincopy
其实还有另外一个方案:线程锁共享。这是什么呢,我估计了解它的人不多,如果你知道的话,那可以称为Linux开发牛人了。
线程锁就是pthread那一套C函数了:
[html] view plaincopy
但是这只能用在一个进程内的多个线程实现互斥,怎么应用到多进程场合呢,被多个进程共享呢?
很简单,首先需要设置互斥锁的进程间共享属性:
[html] view plaincopy
其次,为了达到多进程共享的需要,互斥锁对象需要创建在共享内存中。
最后,需要注意的是,并不是所有Linux系统都支持这个特性,程序里需要检查是否定义了_POSIX_SHARED_MEMORY_OBJECTS宏,只有定义了,才能用这种方式实现进程间互斥锁。
我们来看看这三套方案的平台移植性。
当两个(或者多个)进程没有特殊关系(比如父子进程共享)时,我们只能通过约定好的名字来访问同一个锁,这就是命名锁。然而,如果我们有其他途径定位一个锁,那么匿名锁是更好的选择。这三套方案是否都支持匿名锁与命名锁呢?
通过sem_open创建命名锁,通过sem_init创建匿名锁,其实sem_init也支持进程内部锁。
semget中的key参数可以看成是名字,所以支持命名锁。该方案不支持匿名锁。
不支持命名锁,支持匿名锁。
在匿名锁与命名锁的支持上,一些方案是有不足的,但这还是小问题,更严重的问题是异常状况下的死锁问题。
与多线程环境不一样的是,在多进程环境中,一个进程的异常退出不会影响其他进程,但是如果使用了进程互斥锁呢?假如一个进程获取了互斥锁,但是在访问互斥资源的代码中crash了,或者遇到信号退出了,那么其他等待同一个锁的进程(内部某个线程)就挂死了。在多线程环境中,程序异常整个进程退出,不需要考虑异常时锁的释放,多进程环境则是一个实实在在的问题。
System V信号量通过UNDO方式可以解决该问题。但是如果考虑到平台兼容性等问题,这三个方案仍不能满足需求,我会接着介绍一种更好的方案。
========================================================================
http://blog.csdn.net/luansxx/article/details/7737899
在《进程互斥锁》中,我们看到了实现进程互斥锁的几个常见方案:Posix信号量、System V信号量以及线程锁共享,并且分析了他们的平台兼容性以及严重缺陷。这里要介绍
一种安全且平台兼容的进程互斥锁,它是基于文件记录锁实现的。
UNIX编程的“圣经”《Unix环境高级编程》中有对文件记录锁的详细描述。
下载链接:http://dl02.topsage.com/club/computer/Unix环境高级编程.rar
记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。对于UNIX,“记录”这个定语也是误用,因为UNIX内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)。
各种UNIX系统支持的记录锁形式:
系统 | 建议性 | 强制性 | fcntl | lockf | flock |
POSIX.1 | * | * | |||
XPG3 | * | * | |||
SVR2 | * | * | * | ||
SVR3 SVR4 | * | * | * | * | |
4.3BSD | * | * | * | ||
4.3BSDReno | * | * | * |
可以看成,记录锁在各个平台得到广泛支持。特别的,在接口上,可以统一于fcntl。
建议性锁和强制性锁之间的区别,是指其他文件操作函数(如open,read、write)是否受记录锁影响,如果是,那就是强制性的记录锁,大部分平台只是建议性的。不过,对实现进程互斥锁而言,这个影响不大。
[cpp] view plaincopy
对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针。
[cpp] view plaincopy
以下说明fcntl函数的三种命令:
如何 基于记录锁实现进程互斥锁?
1、需要一个定义全局文件名,这个文件名只能有相关进程使用。这需要在整个系统做一个规划。
2、规定同一个进程互斥锁对应着该文件的一个字节,字节位置称为锁的编号,这样可以用一个文件实现很多互斥锁。
3、编号要有分配逻辑,文件中要记录已经分配的编号,这个逻辑也要保护,所以分配0号锁为系统锁。
4、为了实现命名锁,文件中要记录名称与编号对应关系,这个对应关系的维护也需要系统锁保护。
这些逻辑都实现在一个FileLocks类中:
[cpp] view plaincopy
这里用了一个FileLocks全局单例对象,它对应的文件名是“/tmp/filelock”,在FileLocks中,分别用alloc()和alloc(keyname)分配匿名锁和命名锁,用free_lock删除锁。free_lock(pos)删除匿名锁,free_lock(keyname)删除命名锁。
对锁的使用通过lock、try_lock、unlock实现,他们都带有一个pos参数,代表锁的编号。
有了FileLocks类作为基础,要实现匿名锁和命名锁就很简单了。
[cpp] view plaincopy
需要注意的是,进程匿名互斥锁需要创建在共享内存上。只需要也只能某一个进程(比如创建共享内存的进程)调用构造函数,其他进程直接使用,同样析构函数也只能调用一次。
命名锁只需要构造函数不同,可以直接继承匿名锁实现
[cpp] view plaincopy
需要注意,命名锁不住析构时删除,因为可能多个对象共享该锁。