Fork之前创建了互斥锁,要警惕死锁问题

news2024/11/17 3:38:01

文章目录

  • Fork之前创建了互斥锁,要警惕死锁问题
  • 使用GDB进行调试
  • 如何解决该问题?
  • 是否还有别的问题?
  • 结论
  • 参考文献

Fork之前创建了互斥锁,要警惕死锁问题

下面的这段代码会导致子进程出现死锁问题,您看出来了吗?

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string>
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\n");
        func(NULL);
        printf("after get lock\n");
        return 0;
    }
    else if(ret > 0)
    {
        pthread_join(tid, 0);
        wait(NULL);
    }
    else
    {
        printf("fork failed\n");
        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.html

[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 = "\001\000\000\000\000\000\000\000\244\214\002\000\001", '\000' <repeats 26 times>, __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\n");
(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 = "\001\000\000\000\000\000\000\000\244\214\002\000\001", '\000' <repeats 26 times>, __align = 1}
(gdb)

到此,我们就验证了我们的分析, 确实时由于锁的状态的继承,导致了子进程的死锁。

如何解决该问题?

使用pthread_atfork函数在fork子进程之前清理一下锁的状态。

#include <pthread.h>

int pthread_atfork(void (*prepare)(void), void (*parent)(void),
                    void (*child)(void));

https://man7.org/linux/man-pages/man3/pthread_atfork.3.html

pthread_atfork()在fork()之前调用,当调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child。

修改之后,代码如下:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string>
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\n");
        func(NULL);
        printf("after get lock\n");
        return 0;
    }
    else if(ret > 0)
    {
        pthread_join(tid, 0);
        wait(NULL);
    }
    else
    {
        printf("fork failed\n");
        exit(1);
    }

    return 0;
}

重新编译并运行,死锁问题解决了。

[root@localhost test3]# ./a.out
before get lock
after get lock

是否还有别的问题?

同样的代码,只是本此将锁增加了"可重入"的属性。我们再看看执行结果。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string>
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\n", 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\n");
        func(NULL);
        printf("after get lock\n");
        return 0;
    }
    else if(ret > 0)
    {
        pthread_join(tid, 0);
        wait(NULL);
    }
    else
    {
        printf("fork failed\n");
        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.c
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-udad9xLR-1674974041614)(null)]

可以看到可重入锁解锁时,确实会有owner的检查。并且会返回EPERM的errno, EPERM=1, 这与我们打印出来的ret=1是相一致的。

结论

  • fork函数执行后,子进程会继承来自父进程中的锁和锁的状态
  • 可重入锁解锁会检查owner, 非owner不能解锁。
  • 在fork之前如果有创建互斥锁, 一定需要小心其状态。

参考文献

https://zhuanlan.zhihu.com/p/343845048

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/184508.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MFC】使用MFC框架(10)

MFC不仅仅是一个类库&#xff0c;而且是一个所谓的“设计框架”&#xff0c;注入了很多开发理念和设计思想。类库与框架的区别可以理解为“食材”与“火锅”套餐的区别——火锅套餐已经标明了开发者必须接受已定的一些规则&#xff0c;包括“Message Mapping消息映射机制”、“…

忽略语法细节,从整体上理解函数

从整体上看&#xff0c;C语言代码是由一个一个的函数构成的&#xff0c;除了定义和说明类的语句&#xff08;例如变量定义、宏定义、类型定义等&#xff09;可以放在函数外面&#xff0c;所有具有运算或逻辑处理能力的语句&#xff08;例如加减乘除、if else、for、函数调用等&…

配置中心-开源系统对比分析

一、为什么需要配置中心 1、配置实时生效 传统的静态配置方式要想修改某个配置只能修改之后重新发布应用&#xff0c;要实现动态性&#xff0c;可以选择使用数据库&#xff0c;通过定时轮询访问数据库来感知配置的变化。轮询频率低感知配置变化的延时就长&#xff0c;轮询频率…

运放电路中电容的作用-运算放大器

在运放电路中&#xff0c;大家可能会经常看到这么几个电容&#xff0c;分别是&#xff1a; 1、电源VCC到地 2、反馈输入输出引脚之间 3、正负两输入端之间的电容 就算不要这几个电容&#xff0c;电路好像也能工作&#xff0c;但电路设计一般都会加上&#xff0c;那么这几个电…

软件无线电之数字下变频(Matlab实例)

软件无线电之数字下变频 1 原理 在通信系统中&#xff0c;为了易于信号发射以及实现信道复用&#xff0c;传输的信号发射频率一般很高。 在接收机中&#xff0c;为了降低信号的载波频率或是直接去除载波频率得到基带信号&#xff0c;通常将接收信号与本地振荡器产生的本振信…

Java循环综合案例

文章目录Java循环综合案例案例一&#xff1a;逢 7 跳过案例二&#xff1a;数组元素求和案例三&#xff1a;判断两个数组是否相同案例四&#xff1a;查找元素在数组中的索引案例五&#xff1a;数组元素反转案例六&#xff1a;评委打分案例七&#xff1a;随机产生验证码Java循环综…

那些年我们拿下了 Zynq

小菜鸟的 Zynq 学习经验分享~ 资料来源&#xff1a;黑金 Zynq7035 开发板配套资料&#xff0c;完全适合于 Zynq 学习。 获取方式&#xff1a;【51爱电子】回复【Zynq7000】即可获取资料链接&#xff01;本资料仅供学习使用&#xff0c;切勿商用。 另外四个是关于 Altera FPGA…

跨域和cookie

本文以前端的视角来探讨浏览器的跨域和cookie问题。 一、跨域 跨域简介&#xff1a; 为什么会出现跨域&#xff1f; 出于浏览器的同源策略限制&#xff0c;浏览器会拒绝跨域请求。 什么情况下出现跨域&#xff1f; 不同源就会跨域。同源即&#xff1a;协议、域名、端口号…

图文详解:箭头函数与常规函数的this指向问题

函数中this的指向问题特别容易让人迷糊&#xff0c;这里用示例来指点迷津&#xff0c;走出迷茫。 常规函数下的this指向 1. 纯粹的函数调用 function test(name) { console.log(name) console.log(this) } test(zjcopy) ; test.call(zjcopy, cuclife-2) ; test.call(fal…

pytesseract 安装错误总结

项目场景&#xff1a; 使用eclipse调用pytesseract接口&#xff0c;进行OCR识别。 在anaconda的python3.6.4版本&#xff0c;安装配置pytesseract 问题描述 pip install pytesseract 报错 错误提醒&#xff1a;pytesseract requires Python >3.7 but the running Python…

【数据结构】顺序栈的原理及实现

【数据结构】顺序栈的原理及实现 1.什么是栈 栈它是一种先进后出的有序列表数据结构。栈是线性表里的元素插入和删除只能在该线性表的同一端进行的一种特殊线性表。该线性表的插入和删除都叫栈顶&#xff0c;也就是变化的一端。另一端是固定不变的成为栈底。根据下图可以看出…

《高性能MySQL》——架构与历史(笔记)

文章目录一、MySQL架构与历史1.1.1 连接管理与安全性1.1.2 优化与执行1.2 并发控制1.2.1 读写锁1.2.2 锁粒度&#xff08;锁模式&#xff09;表锁(table lock)行级锁(row lock)1.3 事务1.3.1 隔离级别READ UNCOMITTED (读未提交)READ COMMITTED (读提交)REPEATABLE READ (可重复…

初识C++(学习计划)

前言 基于对C语言的学习&#xff0c;我将进一步学习C的相关知识。 我们在使用C语言时创建的是.c文件&#xff0c;使用C使用的是.cpp文件&#xff0c;其中p——plus&#xff08;加&#xff0c;的意思&#xff09;&#xff0c;所以cpp就是c。 C是为了解决一些C语言不能解决的问题…

SpringBatch使用(一)

一、SpringBatch简介 1、Spring Batch是一个轻量级&#xff0c;全面的批处理框架&#xff0c;旨在开发对企业系统日常运营至关重要的强大批处理应用程序。Spring Batch构建了人们期望的Spring Framework特性&#xff08;生产力&#xff0c;基于POJO的开发方法和一般易用性&…

docker安装elasticsearch kibana 8.6.0(设置密码+汉化+ik分词器)

安装eskibana安装:拉取镜像并安装设置密码汉化配置ik分词器安装: 记得开放使用的端口,或者关闭防火墙 提示:需要提升虚拟机或者服务器的内存到8G以上 拉取镜像并安装 docker pull elasticsearch:8.6.0 docker pull kibana:8.6.0docker network create es-netdocker run -it…

Itext7在PDF指定位置添加电子公章

目录 1. 电子公章的制作 2. java工具keytool生成p12数字证书文件 3. pom依赖 4. 实体类 5. 工具类及测试示例 6. 效果 1. 电子公章的制作 做章网站&#xff1a;http://seal.biaozhiku.com/ 我们选择圆形印章 然后输入公司名&#xff0c;输入章名输入编码然后点击395生成&…

快速幂及矩阵快速幂分析及代码实现

文章目录前言一、认识快速幂二、快速幂思路及代码三、矩阵快速幂3.1、矩阵乘法代码实现3.2、矩阵快速幂代码实现参考资料前言 在学习Acwing c蓝桥杯辅导课第九讲复杂DP-AcWing 1303. 斐波那契前 n 项和时有使用到矩阵快速幂算法&#xff0c;这里来记录下知识点正好也将快速幂部…

车载以太网 - SomeIP测试专栏 - 详细解析 - 01

对于介绍SomeIP协议&#xff0c;我还是想从最基础的协议解析来&#xff0c;所以今天还是先将SomeIP协议详解给大家列举一下&#xff0c;也方便大家在工作中如果不记得哪些信息随时可以查看学习&#xff0c;也算是留给我自己的笔记吧&#xff0c;毕竟确实容易忘记。 SomeIP数据&…

分布式数据库中间件——Mycat2

一、Mycat2 概述 Mycat是基于java语言编写的数据库中间件&#xff0c;核心功能是分库分表和读写分离&#xff0c;可以将大表水平分割为N个小表。 可以看做为Mysql的数据库服务器&#xff0c;可以用连接Mysql的方式去连接Mycat&#xff0c;端口为8066 二、Mycat的三大作用 2.…

Node 项目中常见的问题及解决方法

1. window和mac下设置NODE_ENV变量的问题 我们都知道在前端项目中会根据不同的环境变量来处理不同的逻辑&#xff0c;在node后端中也一样&#xff0c;我们需要设置本地开发环境、测试环境、 线上环境等&#xff0c;此时有一直设置环境变量的方案是在package.json中的script属性…