最近碰到這麼一個問題:程序先獲得鎖,然後進行一些操作,操作完成之後再把鎖釋放掉,然而在獲得鎖之後進行的一些操作中可能導致程序異常退出(比如段錯誤),可以看出還沒有來得及把鎖釋放進程就蹦掉了,從而導致這個鎖長期沒有被釋放,其他想嘗試獲取鎖的進程都會失敗。
這個問題在多進程模型中很容易出現,下面是一個比較簡單的多進程模型程序例子:
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。
如有更好的解決辦法,歡迎指正~