浅析linux中fork函数

作者:吴新武,华清远见嵌入式学院讲师。

Linux通过clone()系统调用实现fork()、vfork()和__clone()库函数创建新的进程,这个调用通过一系列的参数标志来指明父子进程的共享资源,最终将各自的参数标志位传递给clone,由clone()去调用do_fork()来实现创建新的进程的目的。

do_fork的实现源码在kernel/fork.c文件中,其主要的作用就是复制原来的进程成为另一个新的进程,它完成了整个进程的创建过程。do_fork()的实现主要由以下5个步骤,在分析代码之前,先了解以下do_fork()函数的参数的含义,其参数的含义如下。

clone_flags:该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合。通过clone标志可以有选择的对父进程的资源进行复制。例如CLONE_VM表示共享内存描述符合所有的页表; CLONE_FS共享根目录和当前工作目录所在的表以及权限掩码。

statck_start:子进程用户态堆栈的地址;

regs:指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中;

stack_size:未被使用,通常被赋值为0;

parent_tidptr:父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义;

child_tidptr:子进程在用户态下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义。

函数原型及实现为:

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)
        {
        struct task_struct *p;
        p = copy_process(clone_flags, stack_start, regs, stack_size,
                child_tidptr, NULL, trace); (1)
        if (!IS_ERR(p)) {
        if (clone_flags & CLONE_VFORK) {
                p->vfork_done = &vfork;
                init_completion(&vfork); 
            }
        ……
        }

一、首先调用copy_process()函数

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)

{

//对传入的clone_flag进行检查

//为新进程创建一个内核栈、thread_info结构和task_struct;其值域当前进程的值完全相同(父子进程的描述符此时也相同)

p = dup_task_struct(current);

//判断是否超出进城用户可以拥有的总进城数量,检查是否有权对指定的资源进行操作

if (atomic_read(&p->real_cred->user->processes) >=
                task_rlimit(p, RLIMIT_NPROC)) {
        if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
                p->real_cred->user != INIT_USER)
                goto bad_fork_free;
        }

//在task_struct结构中有一个指针user,该指针指向一个user_struct结构,一个用户的多个进程可以通过user指针共享该用户的资源信息,该结构定义在include/linux/sched.h中,

retval = copy_creds(p, clone_flags);

//copy_creds函数中调用:


//检查创建的进程是否超过了系统进程总量

if (nr_threads >= max_threads)
                 goto bad_fork_cleanup_count;

//获得进程执行域

if (!try_module_get(task_thread_info(p)->exec_domain->module))
                goto bad_fork_cleanup_count;

//调用copy_flags函数更新task_struct结构中flags成员。表明进程是否拥有超级用户权限的PF_SUPERPPRIV标志被清除,表明进程还没有exec()的PF_FORKNOEXEC被设置

copy_flags(clone_flags, p);

//根据clone的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间,代码如下图所示。


//为新进程获取一个有效的PID,调用pid = alloc_pidmap();紧接着使用alloc_pidmap函数为这个新进程分配一个pid。由于系统内的pid是循环使用的,所以采用位图方式来管理,用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。

pid = alloc_pid(p->nsproxy->pid_ns);

//父子进程平分共享的时间片

sched_fork(p, clone_flags);

//返回子进程的指针。

return p;

}

再回到do_fork函数,如果copy_process函数成功返回,新创建的子进程被唤醒并投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能开始向地址空间写入。

二、init_completion(&vfork);

如果clone_flags包含CLONE_VFORK标志,那么将进程描述符中的vfork_done字段指向这个完成量,之后再对vfork完成量进行初始化。完成量的作用是,直到任务A发出信号通知任务B发生了某个特定事件时,任务B才会开始执行;否则任务B一直等待。我们知道,如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。究其原因就是此处vfork完成量所起到的作用:当子进程调用exec函数或退出时就向父进程发出信号。此时,父进程才会被唤醒;否则一直等待。此处的代码只是对完成量进行初始化,具体的阻塞语句则在后面的代码中有所体现。

三、检查子进程是否设置了CLONE_STOPPED标志。


设置了CLONE_STOPPED标志通过sigaddset函数为子进程增加挂起信号。signal对应一个unsigned long类型的变量,该变量的每个位分别对应一种信号。具体的操作是,将SIGSTOP信号所对应的那一位置1。

如果子进程并未设置CLONE_STOPPED标志,那么通过wake_up_new_task函数使得父子进程之一优先运行;否则,将子进程的状态设置为TASK_STOPPED。

四、检查CLONE_VFORK标志被设置

如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。


五、返回pid

return nr; //其中nr最后一次赋值为:nr = task_pid_vnr(p);即子进程的pid号。

至此,fork函数的系统调用过程结束,子进程和父进程各返回一次,子进程返回值为0,父进程返回值为子进程的pid号。应用程序可通过fork的返回值来判断是在子进程中还是父进程中,从而实现多进程程序的编写。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值