Linux线程和fork

 

参考这个博客就可以了:

https://www.cnblogs.com/liyuan989/p/4279210.html

int pthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void));  //线程创建进程    

用pthread_atfork函数最多可以安装3个帮助清理锁的函数。

prepare处理程序:

由父进程,在fork创建子进程前调用。这个fork处理程序的任务是获取父进程定义的所有锁。

parent处理程序:

是在fork创建子进程以后、返回之前在父进程上下文中调用的。这个fork处理程序的任务是对 prepare处理程序获取的所有锁进行解锁。

child处理程序:

在fork返回之前在子进程上下文中调用。与parent fork处理程序一样, child fork处理程序也必须释放prepare fork处理程序获取的所有锁。

注意:

不会出现加锁一次解锁两次的情况,虽然看起来也许会出现。

子进程地址空间在创建时就,得到了父进程定义的所有锁的副本。

因为prepare fork处理程序获取了所有的锁,父进程中的内存和子进程中的内存内容在开始的时候是相同的。当父进程和子进程对它们锁的副本进程解锁的时候,新的内存是分配给子进程的,父进程的内存内容是复制到子进程的内存中(写时复制),所以我们就会陷入这样的假象,看起来父进程对它所有的锁的副本进行了加锁,子进程对它所有的锁的副本进行了加锁。父进程和子进程对在不同内存单元的重复的锁都进行了解锁操作,就好像出现了下列事件序列。

总结:

一般工程里就不会在多线程里面,在来搞多进程,不会自己给自己找麻烦

执行exec后,fork后的新程序可以继承原执行程序的文件锁。但是,如果一个文件描述符设置了close-on-exec标志,在执行exec时,会关闭该文件描述符,所以对应的锁也就被释放了,也就无所谓继承了。

注意:这里的文件锁不是线程的读写锁

 

多线程和fork函数的协作性非常差。 对于多线程和fork, 最重要的建议就是永远不要在多线程程序里面调用fork。
请跟我再念一遍: 永远不要在多线程程序里面调用fork。


Linux的fork函数, 会复制一个进程, 对于多线程程序而言, fork函数复制的是调用fork的那个线程, 而并不复制其他的线程。 fork之后其他线程都不见了。 Linux不存在forkall语义的系统调用, 无法做到将多线程全部复制。


多线程程序在fork之前, 其他线程可能正持有互斥量处理临界区的代码。 fork之后, 其他线程都不见了, 那么互斥量的值可能处于不可用的状态, 也不会有其他线程来将互斥量解锁。
下面用一个例子来描述这种场景:
 

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
static void* worker(void* arg)
{
	pthread_detach(pthread_self());
	for (;;)
	{
		setenv("foo", "bar", 1);
		usleep(100);
	}
	return NULL;
}
static void sigalrm(int sig)
{
	char a = 'a';
	write(fileno(stderr), &a, 1);
}
int main()
{
	pthread_t setenv_thread;
	pthread_create(&setenv_thread, NULL, worker, 0);
	for (;;)
	{
		pid_t pid = fork();
		if (pid == 0)
		{
			signal(SIGALRM, sigalrm);
			alarm(1);
			unsetenv("bar");
			exit(0);
		}
		wait3(NULL, WNOHANG, NULL);
		usleep(2500);
	}
	return 0;
}

上面的代码比较简单, 创建了一个线程周期性地执行setenv函数, 修改环境变量。 主线程会fork子进程, 子进程负责执行unsetenv函数, 同时调用了alarm, 一秒钟后会收到SIGALRM信号。 子进程通过执行signal函数, 注册了SIGALRM信号的处理函数, 即向标准错误打印字母‘a’。


fork创建的子进程在调用alarm注册的闹钟之后, 只执行unsetenv函数, 然后就会调用exit退出。 因此, 在正常情况下子进程很快就会退出, alarm约定的1秒钟时间还未到就退出了。 也就是说, 信号处理函数不应该被执行, 自然也就不应该打印出字母‘a’。
可是实际情况是:

./thread_fork
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^C

原因何在? 在某些情况下, 子进程为什么不能及时退出, 以至于过了1秒之后, 子进程还没有退出?
选择一个阻塞的线程, 用gdb调试下, 看看到底阻塞在何处。

# gdb a.out core 
(gdb) bt
#0  __lll_lock_wait_private () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:95
#1  0x00007f8a9c372046 in _L_lock_740 () from /lib/x86_64-linux-gnu/libc.so.6
#2  0x00007f8a9c371e7a in __unsetenv (name=0x400b24 "bar") at setenv.c:325
#3  0x0000000000400a6d in main ()

可以看出调用unsetenv的时候, 子进程就被卡住了。
为什么?
现在的库函数, 为了做到可重入, 其内部维护的变量通常会使用互斥量来保护。 这些锁对用户一般是透明的, 用户也不关心。 setenv和unsetenv就是这样。 尽管上述代码并没有显式地定义, 但是进程内部已经维护了一个互斥量。


互斥量中维护了一个锁的值: 0表示未上锁, 1表示已上锁但是没有等待线程, 2表示已上锁, 并且有线程等待该锁。 对于我们的例子而言, 由于线程每100微秒就执行一次setenv, 很有可能在主线程调用fork创建子进程的瞬间, 互斥量的值是1。 而这个值1被拷贝到了子进程。


对于父进程而言互斥量的值是1自然没有关系, 因为父进程中有线程worker不停地加锁、 解锁。 但是子进程的情况就不同了, 子进程中没有worker。 子进程自创建成功开始, setenv相关的互斥量的值就一直是1。 子进程调用unsetenv函数时, “地雷”被引爆了。 unsetenv无法获得互斥量, 反而是通过调用futex系统调用陷入休眠, 内核将其挂入对应的等待队列。


父进程的worker线程的解锁操作会唤醒子进程吗?
下面是内核get_futex_key函数中的部分代码:

if (!fshared) {
	if (unlikely(!access_ok(VERIFY_WRITE, uaddr, sizeof(u32))))
		return -EFAULT;
	key->private.mm = mm;
	key->private.address = address;
	get_futex_key_refs(key);
	return 0;
}

新建立的futex使用mm结构指针和地址address作为futex的键值, 由于父子进程之间并不共享mm_struct, 也就是说子进程的futex和父进程futex并不共享等待队列。 换句话说, 父进程通过setenv解锁时, 根本就不会唤醒子进程。 因此, 子进程永远都不可能被唤醒了。
这仅仅是setenv/unsetenv函数, 库函数中类似这种的函数并不少见:

  • malloc函数的内部实现一定会有锁。
  • printf系列的函数, 其他线程可能持有stdout/stderr的锁。
  • syslog函数内部实现也会用到锁。

综合上面的讨论, 唯一安全的做法是, fork之后子进程立即调用exec执行另外的程序, 彻底断绝子进程与父进程之间的关系, 注意是立即, 不要在调用exec之前执行任何语句, 哪怕是不起眼的printf。
 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值