最近项目团队中遇到了这几个API的问题,用某个都解决了一部分问题,
又引入了新问题(例如用fork在句柄多的进程里面会导致系统缓慢, vfork导致了句柄泄漏,clone又导致了后面加锁失败),因此这里仔细分析以下。
离开了内核版本讨论这个问题,都是不准确的,因此,本次分析是针对4.9.30版本内核,如果是其它版本,会指出。 编译&文档 编辑环境是ubuntu 16.04.
首先 对象用户的API: fork vfork clone 在内核中调用的都是 do_fork
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL, 0);
}
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#endif
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
fork:
fork的参数是SIGCHLD,表示在子进程exit时,会发送SIGCHLD信号给父进程。 fork会复制父进程的所有资源,包括: 父进程的堆,代码段,栈, 文件句柄,信号等所有资源。 这个过程,在资源比较多的场景下,会对系统产生波动。 一段fork的代码可以证明这点
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <sys/time.h>
#define ALLOC_MEM_SIZE 20 * 1024 * 1024 //20M
int globalVar = 6666;
int main(int argc, char * argv [])
{
int localVar = 8888;
char * s = (char*)malloc(ALLOC_MEM_SIZE );
memset(s, 0, ALLOC_MEM_SIZE);//make VIRT -> RSS
FILE * fp = fopen("1.txt", "w+");
if(!fp)printf("fopen error\n");
struct timeval startTime;
struct timeval endTime;
long costTime;
gettimeofday(&startTime,NULL);
pid_t id = fork();
if(id < 0) printf("fork error\n");
if(id == 0)
{
gettimeofday(&endTime,NULL);
costTime = (endTime.tv_sec - startTime.tv_sec)*1000*1000 + endTime.tv_usec - startTime.tv_usec;
printf("fork: costTime = %ld\n", costTime);
globalVar++;
localVar++;
printf("fork: child, pid = %d, globalVar = %d, &globalVar = %p, localVar = %d, &localVar = %p\n",
getpid(), globalVar, &globalVar, localVar, &localVar);
sleep(1000);
}
else
{
sleep(1);
printf("fork: parent, pid = %d, globalVar = %d, &globalVar = %p, localVar = %d, &localVar = %p\n",
getpid(), globalVar, &globalVar, localVar, &localVar);
sleep(1000);
}
return 0;
}
从上图可以看出,子进程继承了父进程全局变量(全局数据区) 也继承了父进程的局部变量(stack), 变量的虚拟内存地址虽然一样,但是都是在各自的虚存空间中,也指向不同的物理内存。 试图开启地址随机化(echo 0 >/proc/sys/kernel/randomize_va_space ),让子进程的变量虚拟地址和父进程不一致,但是没有区别,还没理解。
上图可以看见,1.txt这个文件句柄被子进程给继承了。
从上图可以看出, 子进程拷贝了父进程的内存,虚存VIRT一样大小,实际内存(RES)也接近。
下面代码是_do_fork的
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
......
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
从上面的代码,可以看出 vfork 传入了 CLONE_VM 参数,在这里,就不会在拷贝内存了(dup_mm)。
本文针对4.9.30内核版本,详细分析了fork, vfork, clone三个API在创建子进程时的资源复制与管理差异。fork会复制所有资源,可能导致系统波动;vfork不复制内存但可能导致句柄泄漏;而clone在某些情况下导致加锁失败。通过示例代码和内存使用情况展示了它们的工作原理。"
115743265,9278951,Linux上Maven项目部署Tomcat及热部署攻略,"['Linux', 'Java', 'Web开发', 'Maven', 'Tomcat']
9453

被折叠的 条评论
为什么被折叠?



