争论

这两天在研究沙盒的时候,跟同事争论了一下如下场景: 使用clone加上CLONE_FS创建子进程,父进程使用chroot,是否会同时对子进程的文件系统产生影响. 根据man手册对于CLONE_FS的描述,使用CLONE_FS创建子进程,子进程或者父进程任意一个调用chroot, chdir或者umask, 都会对另一进程产生影响. 具体怎么影响需要看一下内核的源码.

系统调用在内核中的实现

Linux的系统调用在定义在include/linux/syscalls.h中,例如clone系统调用,在syscalls.h中的定义为

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef CONFIG_CLONE_BACKWARDS
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *, unsigned long,
int __user *);
#else
#ifdef CONFIG_CLONE_BACKWARDS3
asmlinkage long sys_clone(unsigned long, unsigned long, int, int __user *,
int __user *, unsigned long);
#else
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *,
int __user *, unsigned long);
#endif
#endif

内核中系统调用的实现,一般会使用SYSCALL_DEFINEn宏进行封装,其中的n为系统调用的参数个数;使用该宏时,第一个参数为系统调用的名字,仍以clone为例,对应的SYSCALL_DEFINEn宏的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
unsigned long, tls,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#else
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);
}
#endif

CLONE_FS标志在clone()中的使用

clone()系统调用的实现在kernel/fork.c中,该文件中还实现了fork()vfork(),从该文件中可以确定,forkvforkclone在内核中调用的都是_do_fork()函数,只是传递的参数不同。

函数copy_fs()CLONE_FS标志进行了检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
{
struct fs_struct *fs = current->fs;
if (clone_flags & CLONE_FS) {
/* tsk->fs is already what we want */
spin_lock(&fs->lock);
if (fs->in_exec) {
spin_unlock(&fs->lock);
return -EAGAIN;
}
fs->users++;
spin_unlock(&fs->lock);
return 0;
}
tsk->fs = copy_fs_struct(fs);
if (!tsk->fs)
return -ENOMEM;
return 0;
}

可以看到如果设置了CLONE_FS标志,则将当前进程的fs->users加1,否则调用copy_fs_struct(),并将子进程的fs结构指向其返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct fs_struct *copy_fs_struct(struct fs_struct *old)
{
struct fs_struct *fs = kmem_cache_alloc(fs_cachep, GFP_KERNEL);
/* We don't need to lock fs - think why ;-) */
if (fs) {
fs->users = 1;
fs->in_exec = 0;
spin_lock_init(&fs->lock);
seqcount_init(&fs->seq);
fs->umask = old->umask;

spin_lock(&old->lock);
fs->root = old->root;
path_get(&fs->root);
fs->pwd = old->pwd;
path_get(&fs->pwd);
spin_unlock(&old->lock);
}
return fs;
}

该函数实现非常简单,就是创建新的fs_struct结构体,并把当前进程fs结构中的值,然后返回新的fs结构体。
通过这两个函数,就可以得出结论:

  1. 通过clone创建子进程,未指定CLONE_FS时,子进程拥有自己的fs结构,其初始值与父进程的fs结构相同;双方通过chrootchdir修改fs结构时,均不影响另一个进程。

  2. 指定CLONE_FS时,父子进程结构体中fs字段指向同一块内存区域,所以任何一方使用chrootchdir等修改fs结构时,也会对应一个进程生效。

进程的fork

进程的fork,关键在于进程结构体的复制,而这个过程是在dup_task_struct()中实现的,从forkvforkclone开始,调用顺序为:
clone/fork/vfork -> _do_fork -> copy_process -> dup_task_struct

该函数的实现简单来说分为如下步骤:

  1. 为新的task_struct结构体分配内存
  2. 分配栈空间
  3. 将当前进程的task_struct结构体各个字段赋值给子进程的task_struct(使用*dst = *src
  4. 将新的栈空间赋值给子进程的task_struct->stack

为什么子进程的task_struct结构体被分配了新的内存,却还能在设置了CLONE_FS后与父进程共享task_struct->fs
因为task_struct中,fs的数据类型是struct fs_struct *,所以两个fs实际上指向同一块内存区域。