Fork之前创建了互斥锁,要警惕死锁问题
下面的这段代码会导致子进程出现死锁问题,您看出来了吗? #include #include #include #include #include using std::string; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* func(void* arg) { pthread_mutex_lock(&mutex); for(int i = 0;i < 10; ++i) { sleep(1); } pthread_mutex_unlock(&mutex); return NULL; } int main(void) { pthread_t tid; pthread_create(&tid, NULL, func, NULL); sleep(5); int ret = fork(); if (ret == 0) { printf("before get lock "); func(NULL); printf("after get lock "); return 0; } else if(ret > 0) { pthread_join(tid, 0); wait(NULL); } else { printf("fork failed "); exit(1); } return 0; }
对上述代码进行编译, 并运行: [root@localhost test3]# g++ main.cpp -g [root@localhost test3]# ./a.out before get lock
我们发现子进程始终没有打印出"after get lock"的日志。
对fork熟悉的朋友们应该知道,在fork之后,由于copy-on-write机制,当子进程尝试修改数据时,会导致父子进程的内存分离,这个过程也将父进程中的互斥锁给拷贝了过来,也包括了互斥锁的状态(锁定,释放)。
在父进程启动时,首先创建了一个线程去执行func函数,为了让该线程在fork之前可以被调度执行,使用了sleep函数让主进程中的主线程让出cpu,从而执行func函数,在func函数中对互斥锁进行了加锁。
5s后,主进程的主线程sleep结束,从而执行fork函数,产生了子进程,子进程也继承了父进程中的互斥锁,也继承了该锁的锁定状态,因此尝试加锁时,就会出现死锁问题。
下面通过GDB调试验证我们的分析。 使用GDB进行调试
如果有同志对GDB还不熟悉,请参考 https://wizardforcel.gitbooks.io/100-gdb-tips/content/index.htmlopen in new window [root@localhost test3]# gdb a.out
首先设置同时调试父子进程 (gdb) set detach-on-fork off
接下来,在fork之前下一个断点,然后进行单步调试。 (gdb) b 26 Breakpoint 1 at 0x401217: file main.cpp, line 26. (gdb) r Starting program: /home/work/cpp_proj/test3/a.out [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". [New Thread 0x7ffff7a8c640 (LWP 167076)] Thread 1 "a.out" hit Breakpoint 1, main () at main.cpp:26 26 int ret = fork(); Missing separate debuginfos, use: dnf debuginfo-install glibc-2.34-40.el9.x86_64 libgcc-11.3.1-2.1.el9.x86_64 libstdc++-11.3.1-2.1.el9.x86_64 (gdb) n [New inferior 2 (process 167113)] Reading symbols from /home/work/cpp_proj/test3/a.out... Reading symbols from /lib64/ld-linux-x86-64.so.2... [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". 27 if (ret == 0) { Missing separate debuginfos, use: dnf debuginfo-install glibc-2.34-40.el9.x86_64 libgcc-11.3.1-2.1.el9.x86_64 libstdc++-11.3.1-2.1.el9.x86_64 (gdb) n 33 else if(ret > 0)
单步到这里,子进程已经创建成功, 我们打开另一个窗口查看一下,确实目前父子进程都已经启动了 [root@localhost ~]# ps aux |grep -v grep|grep a.out root 166931 0.3 1.4 180844 55780 pts/0 Sl+ 05:29 0:00 gdb a.out root 167072 0.0 0.0 14020 2220 pts/0 tl 05:29 0:00 /home/work/cpp_proj/test3/a.out root 167113 0.0 0.0 14020 1588 pts/0 t 05:30 0:00 /home/work/cpp_proj/test3/a.out
这个时候,我们打印一下父进程中mutex的状态, 如下所示: (gdb) p mutex $1 = {__data = {__lock = 1, __count = 0, __owner = 167076, __nusers = 1, __kind = 0, __spins = 0, __elision = 0, __list = {__prev = 0x0, __next = 0x0}}, __size = " 01 00 00 00 00 00 00 00244214 02 00 01", " 00" , __align = 1}
因为之前父进程中的线程已经执行了func函数, 因此锁的__lock值为1,即锁定状态,锁的__owner时167076, 说明该锁由父进程所加。
接下来,切换到子进程查看:
单步到执行func函数之前。 (gdb) info inferior Num Description Connection Executable * 1 process 167072 1 (native) /home/work/cpp_proj/test3/a.out 2 process 167113 1 (native) /home/work/cpp_proj/test3/a.out (gdb) inferior 2 [Switching to inferior 2 [process 167113] (/home/work/cpp_proj/test3/a.out)] [Switching to thread 2.1 (Thread 0x7ffff7a90380 (LWP 167113))] #0 0x00007ffff7ba98d7 in _Fork () from /lib64/libc.so.6 (gdb) n Single stepping until exit from function _Fork, which has no line number information. 0x00007ffff7ba96fa in fork () from /lib64/libc.so.6 (gdb) n Single stepping until exit from function fork, which has no line number information. main () at main.cpp:27 27 if (ret == 0) { (gdb) n 28 printf("before get lock "); (gdb) n before get lock 29 func(NULL);
这个时候,我们查看一下子进程中mutex的状态, 可以发现__lock的值为1,说明目前该互斥锁已经被加锁。而且可以看到__owner也属于父进程。 (gdb) p mutex $2 = {__data = {__lock = 1, __count = 0, __owner = 167076, __nusers = 1, __kind = 0, __spins = 0, __elision = 0, __list = {__prev = 0x0, __next = 0x0}}, __size = " 01 00 00 00 00 00 00 00244214 02 00 01", " 00" , __align = 1} (gdb)
到此,我们就验证了我们的分析, 确实时由于锁的状态的继承,导致了子进程的死锁。 如何解决该问题?
使用pthread_atfork函数在fork子进程之前清理一下锁的状态。 #include int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
https://man7.org/linux/man-pages/man3/pthread_atfork.3.htmlopen in new window
pthread_atfork()在fork()之前调用,当调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child。
修改之后,代码如下: #include #include #include #include #include using std::string; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* func(void* arg) { pthread_mutex_lock(&mutex); for(int i = 0;i < 10; ++i) { sleep(1); } pthread_mutex_unlock(&mutex); return NULL; } void clean() { if(pthread_mutex_trylock(&mutex) != 0) { pthread_mutex_unlock(&mutex); } } int main(void) { pthread_t tid; pthread_create(&tid, NULL, func, NULL); sleep(5); pthread_atfork(NULL, NULL, clean); int ret = fork(); if (ret == 0) { printf("before get lock "); func(NULL); printf("after get lock "); return 0; } else if(ret > 0) { pthread_join(tid, 0); wait(NULL); } else { printf("fork failed "); exit(1); } return 0; }
重新编译并运行,死锁问题解决了。 [root@localhost test3]# ./a.out before get lock after get lock是否还有别的问题?
同样的代码,只是本此将锁增加了"可重入"的属性。我们再看看执行结果。 #include #include #include #include #include using std::string; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutexattr_t mta; void* func(void* arg) { pthread_mutex_lock(&mutex); for(int i = 0;i < 10; ++i) { sleep(1); } pthread_mutex_unlock(&mutex); return NULL; } void clean() { if(pthread_mutex_trylock(&mutex) != 0) { int ret = pthread_mutex_unlock(&mutex); printf("ret = %d ", ret); } } int main(void) { //增加可重入的属性 pthread_mutexattr_init(&mta); pthread_mutexattr_settype(&mta, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&mutex, &mta); pthread_t tid; pthread_create(&tid, NULL, func, NULL); sleep(5); pthread_atfork(NULL, NULL, clean); int ret = fork(); if (ret == 0) { printf("before get lock "); func(NULL); printf("after get lock "); return 0; } else if(ret > 0) { pthread_join(tid, 0); wait(NULL); } else { printf("fork failed "); exit(1); } return 0; }
执行结果如下: [root@localhost test3]# ./a.out ret = 1 before get lock
此时发现再次发生了死锁。
原因在于可重入锁解锁必须是相同的线程。子进程中的主线程并非加锁线程,因此无法解锁。
查看glibc中的相关实现:
https://github.com/lattera/glibc/blob/master/nptl/pthread_mutex_unlock.copen in new window
glic-pthread-unlock
可以看到可重入锁解锁时,确实会有owner的检查。并且会返回EPERM的errno, EPERM=1, 这与我们打印出来的ret=1是相一致的。 结论fork函数执行后,子进程会继承来自父进程中的锁和锁的状态 可重入锁解锁会检查owner, 非owner不能解锁。 在fork之前如果有创建互斥锁, 一定需要小心其状态。
副处级公务员,40年工龄,2022年退休,能够领取多少养老金?视频加载中按照现行养老金的执行政策来看,机关事业单位人员如果在2022年办理退休,仍旧在养老金并轨的十年过渡期内,也就是我们常说的退休中人。因此该部分退休人员办理退休手续时,将会按
你想别人的高利息,别人想你的本金你有被骗的经历吗?朋友们,大家好!我是蓝中秋。你有被骗的经历吗?我的一位朋友向我讲述了她亲身经历的被骗3万元的经过。现在将这个故事分享出来,希望对朋友们的投资理财有点帮助。为了记述方便,暂且使用第一
徐工集团差点卖给美国徐工集团几乎便宜地卖给了美国。幸运的是,一个人明白了这个问题的严重性,这让z府意识到了这个问题的严重性。在交易过程中有一个黑幕,非常害怕仔细思考。这个混乱者是徐工的国内竞争对手。徐
明清时代我国北方的国际运输线张库商道明清时代我国北方的国际运输线张库商道李桂仁塞北重镇张家口,群山环抱,峰峦叠障,地势隘峻,东望北京天津,北连内蒙古大草原和西北边疆,战略和交通地位都十分重要,素有神都北门之称。历史上
被飞客推荐无数次的JW万豪,你还没住过吗?三张版图镇楼高空泳池三张版图镇楼窗外风景三张版图镇楼自助餐厅终于入住了心心念念的长沙JW酒店,最近在飞客论坛,长沙JW酒店也频繁出现在大家的视野里。地处南二环涂家冲的JW酒店位置相
非比寻常秋冬旅行,金陵四十八景中的池寺山,空有遗迹人相依大风起兮云飞扬,家住六朝烟水间。这里是胖一和雪酱的探游日记,跟您分享诗词旅游中的所见所闻所感。这是一次非比寻常的秋冬旅行,池寺山,整个一次收集。这里,曾经是金陵四十八景之一,如诗如
长期吃花生是降血压,还是升血压?一文讲清楚了花生素有长寿果的美誉。逢年过节很多家庭都会买一些花生,这也算是常见的坚果,营养丰富,不仅可以做下酒菜,更可做烹饪的辅助材料,制作成各种各样的美食。经常有人说每天吃一把花生能够养胃,
胃不舒服,三分靠治,七分靠养三餐不规律,火锅配冷饮,饭后犯懒不想动,挨饿经常过了头。现代人生活越来越好,但肠胃却被自己作得越来越差。这肠胃问题就像那温水煮青蛙,一开始的放肆会慢慢导致不适。这加热的温水,是否已
皮蛋,到底是人间美味还是健康杀手呢?现在知道不算晚皮蛋是一种独特的食物,但厌恶皮蛋的人不在少数,甚至有些人给皮蛋贴上了最恶心的食物的评价,这皮蛋真的就那么可怕吗?皮蛋本身是中国人餐桌上常吃的食物,如今看到皮蛋的评价不好,又了解了皮
靠政策兜底,中国足球何以脱贫攻坚在众多的体育项目里,中国足球肯定是困难户中的困难户我们能消灭困扰几千年的贫困,却对中国足球束手无策我们不尊重规则与规律,我们却喜欢制定政策我们以为保护了中国足球的希望,却在不知不觉
马斯克入主推特之后,已解雇公司CEO,接下来模仿中国微信?什么是生意人?就是不断生出主意的人!世人皆知,马斯克是全球首富,他虽然很有钱,但人家的钱也不是大风刮来的,所以马斯克也是非常懂得生意场上的成交之道!马斯克很早就钟情于推特了。今年4