muti-thread & fork

1 要点

fork会共用原来的代码段,对于数据段和堆栈进行“写时拷贝”, 对于内核全局变量应用,例如文件句柄进行+1。

因此fork会产生一个和原来进程占用内存一样的进程,注意只是和原来进程的内存模型一样,而不会产生和父进程一样的多线程进程,fork后的子进程会成为一个单线程进程,其他线程默认终止,这个单线程即是发生fork调用时的线程。


2  原型分析

在kernel/fork.c我们找到了fork函数原型:

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	...

	p = copy_process(clone_flags, stack_start, regs, stack_size,
			 child_tidptr, NULL, trace);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	...
}
省略的部分为标志判断,和新进程任务调度代码,其核心工作都是由copy_process完成。

static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					struct pt_regs *regs,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace)
{
...

	retval = security_task_create(clone_flags);
	if (retval)
		goto fork_out;

	retval = -ENOMEM;
	p = dup_task_struct(current);
	if (!p)
		goto fork_out;

	ftrace_graph_init_task(p);

	rt_mutex_init_task(p);

...

	/* Perform scheduler related setup. Assign this task to a CPU. */
	sched_fork(p);

	retval = perf_event_init_task(p);
	if (retval)
		goto bad_fork_cleanup_policy;
	retval = audit_alloc(p);
	if (retval)
		goto bad_fork_cleanup_policy;
	/* copy all the process information */
	retval = copy_semundo(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_audit;
	retval = copy_files(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_semundo;
	retval = copy_fs(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_files;
	retval = copy_sighand(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_fs;
	retval = copy_signal(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_sighand;
	retval = copy_mm(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_signal;
	retval = copy_namespaces(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_mm;
	retval = copy_io(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_namespaces;
	<strong>retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);</strong>
...
}
为了结构清晰,省略了大量代码。

int copy_thread(unsigned long clone_flags, unsigned long sp,
	unsigned long unused,
	struct task_struct *p, struct pt_regs *regs)
{
	struct pt_regs *childregs;
	struct task_struct *tsk;
	int err;

	childregs = task_pt_regs(p);
	*childregs = *regs;
	childregs->ax = 0;
	childregs->sp = sp;

	p->thread.sp = (unsigned long) childregs;
	p->thread.sp0 = (unsigned long) (childregs+1);

	p->thread.ip = (unsigned long) ret_from_fork;

	task_user_gs(p) = get_user_gs(regs);

	p->fpu_counter = 0;
	p->thread.io_bitmap_ptr = NULL;
	tsk = current;
	err = -ENOMEM;

	memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

	if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
		p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
						IO_BITMAP_BYTES, GFP_KERNEL);
		if (!p->thread.io_bitmap_ptr) {
			p->thread.io_bitmap_max = 0;
			return -ENOMEM;
		}
		set_tsk_thread_flag(p, TIF_IO_BITMAP);
	}

	err = 0;

	/*
	 * Set a new TLS for the child thread?
	 */
	if (clone_flags & CLONE_SETTLS)
		err = do_set_thread_area(p, -1,
			(struct user_desc __user *)childregs->si, 0);

	if (err && p->thread.io_bitmap_ptr) {
		kfree(p->thread.io_bitmap_ptr);
		p->thread.io_bitmap_max = 0;
	}
	return err;
}
copy_thread的主要工作室设置线程栈, tls, 寄存器等信息。

从上面可以看出,对于多线程fork,并不会产生一个多线程进程,只会产生一个和多线程占用内存一样大小的单线程进程,posix线程id即是父线程中的posix 线程id。


3 测试结论

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>

#define __NR_gettid 186
void *f1()
{
	printf("tid:%ld\n", pthread_self());
	sleep(100000000);
}

int main()
{
	int i = 0;
	pthread_t pth1[20]; 
	while(i++<20){
		pthread_create(&pth1[i], NULL, f1, NULL);
		sleep(1);
	}

	printf("create thread finish!!!\n");
	sleep(10);
	
	int status;
	int ret = fork();
	if(ret == 0){
		printf("child: parent pid: %d, tid:%ld\n", getpid(), pthread_self());
		sleep(30);
		printf("clild exit.");
		return;
	}else if(ret > 0){
		printf("parent: parent pid: %d, tid:%ld\n", getpid(), pthread_self());
		waitpid(-1, &status, 0);
	}
	pause();
}


由上图也可以看出,父进程有20个线程,子线程只有一个线程,但他们占用的内存一样大。

4 总结

多线程中调用fork并不会导致内存泄露,因为子进程退出后,所有资源由系统自动销毁,但是如果子进程进入死循环,则有可能导致资源不足。

另一方面,由于子进程复制父进程的内存及变量信息,会导致一些全局锁,信号量重复锁定的问题。所以尽量不要在多线程中调用fork,如果必须,在调用fork后立即调用exec覆盖子进程是一个不错的方案,对于无法立即执行exec的程序,需要调用pthread_atfork()进行各个资源的释放。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值