`

线程和 fork

阅读更多
    父进程调用 fork 创建的子进程会继承整个地址空间的副本,以及每个互斥量、读写锁和条件变量的状态。如果父进程包含一个以上的线程,子进程在 fork 返回后,如果不是紧接着调用 exec 的话,就需要清理锁状态。因为在子进程内部,只存在父进程中调用 fork 的线程的副本一个线程。如果父进程中的线程占有锁,子进程将同样占有这些锁。问题是子进程可能并不包含占有锁的线程的副本,所以它就没法知道它占有了哪些锁、需要释放哪些锁。
    因此在多线程的进程中,为了避免不一致状态的问题,POSIX.1 声明,在 fork 返回和子进程调用 exec 族函数之间,子进程只能调用异步信号安全的函数。这就限制了在调用 exec 之前子进程能做什么,但不涉及锁状态的问题。要清除锁状态,可以调用 pthread_atfork 函数建立 fork 处理程序(对于条件变量,由于它可能是使用全局锁来保护,也可能是直接把锁嵌入到条件变量的数据结构中,目前还没有可移植的方法对这样的锁进行状态清理)。
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
                                  /* 返回值:若成功,返回 0;否则,返回错误编号 */

    使用该函数一次最多可以安装 3 个清理锁的函数。其中 prepare 函数由父进程在 fork 创建子进程前调用,用来获取父进程定义的所有锁。parent 函数是在 fork 创建子进程之后、返回之前在父进程上下文中调用的,用来对 prepare 获取的所有锁进行解锁。child 函数则是在 fork 返回之前在子进程上下文中调用,用来释放 prepare 获取的所有锁。
    可以多次调用该函数来设置多套 fork 处理程序,对不需要的某个处理程序可以在对应位置传入空指针。多个 fork 处理程序的调用顺序是不相同的。parent 和 child 是以它们注册时的顺序调用,而 prepare 的调用则与注册顺序相反。这样可以允许多个模块注册它们自己的 fork 处理程序,而且可以保持锁的层次。例如,假设模块 A 调用模块 B 中的函数,而且每个模块有自己的一套锁。如果锁的层次是 A 在 B 之前,则模块 B 必须在模块 A 之前设置它的 fork 处理程序。当父进程调用 fork 时,就会执行以下的步骤(假设子进程在父进程之前执行):
    (1)调用模块 A 的 prepare 来获取模块 A 的所有锁。
    (2)调用模块 B 的 prepare 来获取模块 B 的所有锁。
    (3)创建子进程。
    (4)调用模块 B 中的 child 处理程序来释放子进程中模块 B 的所有锁。
    (5)调用模块 A 中的 child 处理程序来释放子进程中模块 A 的所有锁。
    (6)fork 函数返回到子进程。
    (7)调用模块 B 中的 parent 处理程序来释放父进程中模块 B 的所有锁。
    (8)调用模块 A 中的 parent 处理程序来释放父进程中模块 A 的所有锁。
    (9)fork 函数返回到父进程。
    下面的程序描述了如何使用 pthread_atfork 和 fork 处理程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t	lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void prepare(void){
	printf("prepare acquiring locks\n");
	pthread_mutex_lock(&lock1);
	pthread_mutex_lock(&lock2);
}

void parent(void){
	printf("parent unlocking locks\n");
	pthread_mutex_unlock(&lock1);
	pthread_mutex_unlock(&lock2);
}

void child(void){
	printf("child unlocking locks\n");
	pthread_mutex_unlock(&lock1);
	pthread_mutex_unlock(&lock2);
}

void *thr_fn(void *arg){
	printf("thread started\n");
	pause();
	printf("thread ended\n");  // can't reach here, for no signal handlers.
	return (void *)0;
}

int main(void){
	if(pthread_atfork(prepare, parent, child) != 0){
		printf("pthread_atfork error\n");
		exit(1);
	}
	pthread_t	tid;
	if(pthread_create(&tid, NULL, thr_fn, NULL) != 0){
		printf("pthread_create error\n");
		exit(1);
	}
	sleep(2);		// Not reliably waiting for thread to run.
	printf("parent process about to call fork()...\n");
	pid_t	pid;
	if((pid = fork()) < 0){
		printf("fork error\n");
		exit(1);
	}
	if(pid == 0)
		printf("child returned from fork\n");
	else
		printf("parent returned from fork\n");
	exit(0);
}

    运行结果如下:
$ ./atforkDemo.out 
thread started
parent process about to call fork()...
prepare acquiring locks
parent unlocking locks
parent returned from fork
child unlocking locks
child returned from fork

    由此可见 prepare 处理程序在调用 fork 之后运行,child 在调用返回到子进程之前执行,parent 在 fork 调用返回给父进程之前运行。
    虽然 pthread_atfork 的意图是使 fork 之后的锁状态保持一致,但它还是存在下面这些不足之处,因此只能在有限情况下可用。
   (1)没有很好的办法对较复杂的同步对象(如条件变量和屏障)进行状态的重新初始化。
   (2)某些错误检查的互斥量实现在 child fork 处理程序试图对被父进程加锁的互斥量进行解锁时会产生错误。
   (3)递归互斥量不能在 child fork 处理程序中清理,因为没法确定加锁的次数。
   (4)如果子进程只允许调用异步信号安全的函数,child fork 处理程序就不可能清理同步对象,因为用于操作清理的所有函数都不是异步信号安全的。实际的问题是同步对象在某个线程调用 fork 时可能处于中间状态,除非同步对象处于一致状态,否则无法被清理。
   (5)如果应用程序在信号处理程序中调用了 fork(这是合法的,因为 fork 本身是异步信号安全的),则 pthread_atfork 注册的 fork 处理程序只能调用异步信号安全的函数,否则结果将是未定义的。
分享到:
评论

相关推荐

    linux 进程 线程 fork 的深入思考

    linux 进程 线程 fork 的深入思考 一道面试题的思考

    LINUX环境高级编程 第六章 线程管理

    线程的概念 线程的创建 线程的终止 线程的同步 线程属性 同步属性 取消选项 线程和信号 线程和fork

    Python在UNIX和Linux系统管理指南 中文版

    ·使用多线程和fork选项。 ·使用网络设备从一个进程获取另一个进程的信息。 ·创建可点击的、易于交互的GUI工具。 ·通过交互式SNMP编程实现监控大型主机集群。 ·掌握IPython shell,作为Bash、Korn或Z-Shell的...

    python unix linux 系统管理指南

    python unix linux 系统管理指南 本书介绍了python语言如何为管理unix和linux服务器提供各种更加有效的任务处理方式...·使用多线程和fork选项。 ·使用网络设备从一个进程获取另一个进程的信息。 ·创建可点击的、

    《Python UNIX 和Linux 系统管理指南》[PDF]

    ·使用多线程和fork选项。 ·使用网络设备从一个进程获取另一个进程的信息。 ·创建可点击的、易于交互的gui工具。 ·通过交互式snmp编程实现监控大型主机集群。 ·掌握ipython shell,作为bash、korn或z-shell的...

    Python.Unix和Linux系统管理指南

    使用多线程和fork选项。 使用网络设备从一个进程获取另一个进程的信息。 创建可点击的、易于交互的GUl工具。 通过交互式SNMP编程实现监控大型主机集群。 掌握IPython shell,作为Bash、Korn或Z—Shell的替换或补充。...

    PYTHON UNIX和LINUX系统管理指南

    , 使用多线程和fork选项。, 使用网络设备从一个进程获取另一个进程的信息。, 创建可点击的、易于交互的GUl工具。, 通过交互式SNMP编程实现监控大型主机集群。, 掌握IPython shell,作为Bash、Korn或Z—Shell的替换或...

    Python.Unix和Linux系统管理指南 pdf

    ·使用多线程和fork选项。 ·使用网络设备从一个进程获取另一个进程的信息。 ·创建可点击的、易于交互的gui工具。 ·通过交互式snmp编程实现监控大型主机集群。 ·掌握ipython shell,作为bash、korn或z-shell的...

    UNIX环境高级编程_第二版中文

    12.9 线程和fork  12.10 线程和I/O  12.11 小结  习题  第13章 守护进程  13.1 引言  13.2 守护进程的特征  13.3 编程规则  13.4 出错记录  13.5 单实例守护进程  13.6 守护进程的惯例  13.7...

    UNIX环境高级编程(第二版中文)

    12.9 线程和fork 336 12.10 线程和I/O 339 12.11 小结 340 习题 340 第13章 守护进程 341 13.1 引言 341 13.2 守护进程的特征 341 13.3 编程规则 342 13.4 出错记录 345 13.5 单实例守护进程 348...

    unix环境编程电子书

    313 12.3 线程属性 314 12.4 同步属性 318 12.5 重入 324 12.6 线程私有数据 328 12.7 取消选项 331 12.8 线程和信号 333 12.9 线程和fork 336 12.10 线程和I/O 339 12.11 小结 340 习题 340 第...

    UNIX环境高级编程

    12.9 线程和fork 336 12.10 线程和I/O 339 12.11 小结 340 习题 340 第13章 守护进程 341 13.1 引言 341 13.2 守护进程的特征 341 13.3 编程规则 342 13.4 出错记录 345 13.5 单实例守护进程 348...

    UNIX环境高级编程_第2版.part1

    12.9 线程和fork 336 12.10 线程和i/o 339 12.11 小结340 习题340 第13章守护进程341 13.1 引言341 13.2 守护进程的特征341 13.3 编程规则342 13.4 出错记录345 13.5 单实例守护进程348 13.6 守护进程的...

    UNIX环境高级编程_第2版.part2

    12.9 线程和fork 336 12.10 线程和i/o 339 12.11 小结340 习题340 第13章守护进程341 13.1 引言341 13.2 守护进程的特征341 13.3 编程规则342 13.4 出错记录345 13.5 单实例守护进程348 13.6 守护进程的...

    分而治之,ForkJoin,多线程编程

    分而治之,ForkJoin,多线程编程

    php多进程框架-模拟java多线程接口simple-fork-php.zip

    SimpleFork提供一套类似于JAVA多线程的进程控制接口,提供回收、同步、互斥、通信等方案,开发者可以关注业务问题,不需要过多考虑进程控制。 引入 composer require jenner/simple_fork require path/to/...

    Linux下Fork与Exec使用

    fork函数是Unix系统最杰出的成就之一,它是七十年代UNIX早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的...

Global site tag (gtag.js) - Google Analytics