最近碰到这么一个问题:程序先获得锁,然后进行一些操作,操作完成之后再把锁释放掉,然而在获得锁之后进行的一些操作中可能导致程序异常退出(比如段错误),可以看出还没有来得及把锁释放进程就蹦掉了,从而导致这个锁长期没有被释放,其他想尝试获取锁的进程都会失败。
这个问题在多进程模型中很容易出现,下面是一个比较简单的多进程模型程序例子:
dead_lock.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "mylock.h"
#define MAX_PROCESS 32
#define PROC_NUMBER 5
typedef struct process_map {
int index;
pid_t pid;
} process_map_t;
pid_t pid;
int proc_index;
process_map_t process_pids[MAX_PROCESS];
int is_restarting = 0;
char *test_data[PROC_NUMBER];
void work_loop()
{
char c;
while (1) {
//do something
printf("proc_index: %d, pid: %d, mylock\n", proc_index, pid);
mylock();
//获取第一个字符。这里只是模拟,真正的工作要比这个复杂多了
c = *test_data[proc_index];
printf("proc_index: %d, pid: %d, c: %c\n", proc_index, pid, c);
myunlock();
printf("proc_index: %d, pid: %d, myunlock\n", proc_index, pid);
sleep(20);
}
}
int main(int argc, char *argv[])
{
int proc, i, ret, status;
memset(process_pids, 0, sizeof(process_pids));
if (mylock_init() < 0) {
exit(1);
}
for (proc = 0; proc < PROC_NUMBER; proc++) {
if (proc == 2) {
//让第3个子进程访问NULL指针而出现段错误
test_data[proc] = NULL;
} else {
test_data[proc] = "xxxxx";
}
}
for (proc = 0; proc < PROC_NUMBER; proc++) {
ret = fork();
if (ret == 0) {
//child process
break;
} else if (ret < 0) {
exit(1);
}
process_pids[proc].index = proc;
process_pids[proc].pid = ret;
}
proc_index = proc;
if (proc == PROC_NUMBER) {
//parent process
while (1) {
for (i = 0; i < PROC_NUMBER; i++) {
if ((ret = waitpid(process_pids[i].pid, &status, WNOHANG)) == 0) {
continue;
}
ret = fork();
if (ret == 0) {
proc_index = process_pids[i].index;
is_restarting = 1;
printf("proc_index: %d, pid: %d, is_restarting\n", proc_index, getpid());
goto CHILD_START;
} else if (ret < 0) {
exit(1);
}
process_pids[i].pid = ret;
}
sleep(5);
}
}
CHILD_START:
pid = getpid();
work_loop();
exit(0);
}
mylock.h:
#ifndef __MYLOCK_H__
#define __MYLOCK_H__
int mylock_init(void);
void mylock(void);
void myunlock(void);
#endif
mylock.c:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include "mylock.h"
static pthread_mutex_t *mptr;
int mylock_init(void)
{
pthread_mutexattr_t mattr;
mptr = mmap(0, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
if (mptr == MAP_FAILED) {
return -1;
}
pthread_mutexattr_init(&mattr);
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(mptr, &mattr);
return 1;
}
void mylock(void)
{
pthread_mutex_lock(mptr);
}
void myunlock(void)
{
pthread_mutex_unlock(mptr);
}
以上代码中,main函数主要是初始化test_data数组,故意将下标为2的指针设成NULL,让访问它的进程产生段错误,之后fork出5个子进程,并把进程号和进程索引放到process_ids数组中,然后父进程每隔5秒用waitpid检测子进程有没有退出,退出则重新fork一个子进程来继续工作,而子进程就直接进入work_loop函数做自己的工作,work_loop函数里面处理test_data数组时用了锁(其实test_data不是共享内存数据可以不用锁的,这里只是模拟一下就当test_data是共享数据吧),mylock.c文件里用pthread_mutex_lock和pthread_mutex_unlock来实现一个多进程间的锁,也可以用gcc的原子操作来实现。下面是编译运行结果:
可以看出第2号进程pid为31530已经重启了,由于退出时没有释放掉锁,所以0-4号进程都没办法再次获得锁了,这也是意料之中。
本文就是来解决这个问题的,我想到的解决办法有以下这么几个:
1、进程重启后释放掉重启之前拿到的锁
因为没有哪个进程去释放锁从而这个锁就被长期锁住了,解决办法之一就是死了的进程在重启之后要自己去把重启之前拿到的锁释放掉,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "mylock.h"
#define MAX_PROCESS 32
#define PROC_NUMBER 5
typedef struct process_map {
int index;
pid_t pid;
} process_map_t;
pid_t pid;
int proc_index;
process_map_t process_pids[MAX_PROCESS];
int is_restarting = 0;
int *hold_lock_proc = NULL;
char *test_data[PROC_NUMBER];
void work_loop()
{
char c;
//如果进程重启,而且是自己进程拿到锁的话就先把锁释放掉
if (is_restarting == 1 && *hold_lock_proc == proc_index) {
myunlock();
}
is_restarting = 0;
while (1) {
//do something
printf("proc_index: %d, pid: %d, mylock\n", proc_index, pid);
mylock();
//保存拿到锁的进程索引
*hold_lock_proc = proc_index;
//获取第一个字符
c = *test_data[proc_index];
printf("proc_index: %d, pid: %d, c: %c\n", proc_index, pid, c);
myunlock();
printf("proc_index: %d, pid: %d, myunlock\n", proc_index, pid);
sleep(20);
}
}
int main(int argc, char *argv[])
{
int proc, i, ret, status;
memset(process_pids, 0, sizeof(process_pids));
hold_lock_proc = mmap(0, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
if (hold_lock_proc == MAP_FAILED) {
exit(1);
}
if (mylock_init() < 0) {
exit(1);
}
for (proc = 0; proc < PROC_NUMBER; proc++) {
if (proc == 2) {
//让第3个子进程访问NULL指针而出现段错误
test_data[proc] = NULL;
} else {
test_data[proc] = "xxxxx";
}
}
for (proc = 0; proc < PROC_NUMBER; proc++) {
ret = fork();
if (ret == 0) {
//child process
break;
} else if (ret < 0) {
exit(1);
}
process_pids[proc].index = proc;
process_pids[proc].pid = ret;
}
proc_index = proc;
if (proc == PROC_NUMBER) {
//parent process
while (1) {
for (i = 0; i < PROC_NUMBER; i++) {
if ((ret = waitpid(process_pids[i].pid, &status, WNOHANG)) == 0) {
continue;
}
ret = fork();
if (ret == 0) {
proc_index = process_pids[i].index;
is_restarting = 1;
printf("proc_index: %d, pid: %d, is_restarting\n", proc_index, getpid());
goto CHILD_START;
} else if (ret < 0) {
exit(1);
}
process_pids[i].pid = ret;
}
sleep(5);
}
}
CHILD_START:
pid = getpid();
work_loop();
exit(0);
}
每个进程拿到锁之后要用一个变量来保存拿到锁的进程索引号,以便进程重启之后比较是不是自己之前拿到锁没释放就退出了,因为是多个子进程共享的变量,所以main函数开始时得先mmap出一个4字节的int类型共享内存来保存这个进程索引号。在work_loop函数中判断如果是进程重启过而且是自己拿过锁没释放,那就先把锁释放掉。下面是编译运行结果:
可以看出第2号进程重启之后其他进程也能获得锁了,只是第2号进程一直重复产生段错误而重启,这也是意料之中的。
这种方式在一定程度上解决了锁长期被占从而导致类似死锁的问题,但是在进程退出到重启之后再释放锁期间时间可能比较长,这样其他进程获取锁的时间就有点长了,但总比一直等锁强吧。还有一个缺点是如果程序中用到的锁比较多或者有嵌套锁的话,就比较难管理了,需要用一个列表来保存该进程拿到的锁,而且这个列表也要放到共享内存中,否则进程重启之后无法访问列表也白搭。
2、超时强制释放
这个方法比较暴力,有可能误伤,要做好评估,评估锁最长会占用多长时间,超过这个时间就可以强制释放锁了,这样锁就得重新实现而不能用上面那种pthread_mutex_lock和pthread_mutex_unlock了,需要自己用共享内存以及gcc的原子操作来实现,以及加入超时机制,这个可以参考nginx的ngx_shmtx.c里面的实现,这里就不展开了。
3、注册SIGSEGV信号处理函数,发现段错误时先把锁释放了再退出
这个方法比第一种方法稍微好一点,只是锁释放的时机提前了一点而已。看代码吧:
dead_lock_free.c :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "mylock.h"
#define MAX_PROCESS 32
#define PROC_NUMBER 5
typedef struct process_map {
int index;
pid_t pid;
} process_map_t;
pid_t pid;
int proc_index;
process_map_t process_pids[MAX_PROCESS];
int is_restarting = 0;
int *hold_lock_proc = NULL;
char *test_data[PROC_NUMBER];
void segment_fault_handler(int sig)
{
printf("segment_fault_handler\n");
//如果是自己获取了锁,则先把锁释放了再退出
if (proc_index == *hold_lock_proc) {
printf("proc_index: %d, pid: %d, myunlock before exit\n", proc_index, pid);
myunlock();
}
//恢复SIGSEGV信号
signal(SIGSEGV, SIG_DFL);
}
void work_loop()
{
char c;
while (1) {
//do something
printf("proc_index: %d, pid: %d, mylock\n", proc_index, pid);
mylock();
//保存拿到锁的进程索引
*hold_lock_proc = proc_index;
//获取第一个字符
c = *test_data[proc_index];
printf("proc_index: %d, pid: %d, c: %c\n", proc_index, pid, c);
myunlock();
printf("proc_index: %d, pid: %d, myunlock\n", proc_index, pid);
sleep(20);
}
}
int main(int argc, char *argv[])
{
int proc, i, ret, status;
memset(process_pids, 0, sizeof(process_pids));
//申请一个匿名共享内存来保存获得锁的进程索引
hold_lock_proc = mmap(0, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
if (hold_lock_proc == MAP_FAILED) {
exit(1);
}
//注册SIGSEGV信号
if (signal(SIGSEGV, segment_fault_handler) == SIG_ERR) {
perror("signal error: ");
}
if (mylock_init() < 0) {
exit(1);
}
for (proc = 0; proc < PROC_NUMBER; proc++) {
if (proc == 2) {
//让第3个子进程访问NULL指针而出现段错误
test_data[proc] = NULL;
} else {
test_data[proc] = "xxxxx";
}
}
for (proc = 0; proc < PROC_NUMBER; proc++) {
ret = fork();
if (ret == 0) {
//child process
break;
} else if (ret < 0) {
exit(1);
}
process_pids[proc].index = proc;
process_pids[proc].pid = ret;
}
proc_index = proc;
if (proc == PROC_NUMBER) {
//parent process
while (1) {
for (i = 0; i < PROC_NUMBER; i++) {
if ((ret = waitpid(process_pids[i].pid, &status, WNOHANG)) == 0) {
continue;
}
ret = fork();
if (ret == 0) {
proc_index = process_pids[i].index;
is_restarting = 1;
printf("proc_index: %d, pid: %d, is_restarting\n", proc_index, getpid());
goto CHILD_START;
} else if (ret < 0) {
exit(1);
}
process_pids[i].pid = ret;
}
sleep(5);
}
}
CHILD_START:
pid = getpid();
work_loop();
exit(0);
}
代码里面也加注释了,就不细说了。下面是编译运行结果:
可见各个进程都有机会拿到锁,没有发生死锁的情况。
还有一个终极解决办法是:不要让程序产生段错误,尽量把锁的粒度放到最小,一是减少锁的开销,二是减少发生本文开头所说问题的概率。不要让程序产生段错误这个就得看个人内功和团队的整个能力了,另外也要多做code review。
如有更好的解决办法,欢迎指正~