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()进行各个资源的释放。