进程的创建

Linux内核把进程称为任务(task)。在介绍进程之前,介绍一下linux源码的结构。

源码结构

注意带“/”的都是目录,其他的是文件。

  • arch/ :不同架构、平台体系相关代码,如s390、x86
  • block/ :块设备驱动
  • certs/ :与认证签名相关的代码
  • cryto/ :内核常用的加密和压缩算法等代码
  • Documentation/ :描述模块功能和协议规范的代码
  • drivers/ :驱动程序,如usb总线、pci总线、显卡驱动
  • firmware/ :主要是一些二进制固件的代码
  • fs/ :虚拟文件系统的代码
  • include/ :内核源码依赖的绝大部分头文件
  • init/ :内核初始化代码,联系到内存各组件入口
  • ipc/ :进程间通信的实现代码,比如共享内存、信号量、匿名管道等
  • kernel/ :内核核心代码,包括进程管理、时间、中断等
  • lib/ :c标准库的子集
  • mm/ :内存管理要实现的相关操作
  • net/ :网络协议代码,如tcp、udp、ipv6、wifi、以太网等
  • samples/ :内核实例代码
  • scripts/ :编译和配置内核所需脚本
  • security/ :内核安全模型相关代码,如selinux
  • sound/ :声卡驱动源码
  • tools/ :与内核交互的相关工具
  • usr/ :用户打包和压缩内核的实现源码
  • virt/ :/kvm虚拟化目录相关支持实现
  • COPYING :许可和授权信息
  • CREDITS :贡献者列表
  • Kbuild :内核设定脚本
  • Kconfig :开发人员配置内核时所用文件
  • MAINTAINERS :目前维护者列表
  • Makefile :编译内核的主要文件
  • README :编译内核信息
  • REPORTINGVM-BUGS :如何上报bug

进程

进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户空间虚拟地址空间。
进程有两种特殊形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。通用在不会引起混淆的情况下把用户线程简称为线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
Linux进程有四要素:

  1. 有一段程序供其执行。
  2. 有进程专用的系统堆栈空间。
  3. 在内核有task_struct数据结构。
  4. 有独立的存储空间,拥有专有的用户空间。

如果只具备前三条而缺少第四条,则称为“线程”。如果完全没有用户空间,就称为“内核线程”;而如果共享用户空间映射就称为“用户线程”。
内核为每个进程分配一个task_struct结构时。实际分配两个连续物理页面(8192字节),数据结构task_struct的大小约占1kb字节左右,进程的系统空间堆栈的大小约为7kb字节(不能扩展,是静态确定的)。

task_struct

每个进程/线程都有一个对应的task_struct结构体。下面只介绍较为重要的结构。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
    /*
     * For reasons of header soup (see current_thread_info()), this
     * must be the first element of task_struct.
    */
    struct thread_info thread_info;
#endif

    /* -1 unrunnable, 0 runnable, >0 stopped: */
    volatile long state; //进程状态
    
    void *stack; //指向内核栈
    
    //调度策略和优先级
    int prio;
    int static_prio;
    int normal_prio;
    unsigned int rt_priority;
    
    const struct sched_class *sched_class; //调度类,注意是调度类包含进程
    cpumask_t cpus_allowed; // 允许进程在哪些处理器运行

    //指向内存描述符
    //mm和active_mm指向同一内存。对于内核线程而言,mm是空指针;当内核线程运行时,active_mm指向从进程借用的内存描述符
    struct mm_struct *mm;
    struct mm_struct *active_mm;
    pid_t pid; //全局进程号
    pid_t tgid; //全局的线程组标识符
    
    /*
     * Pointers to the (original) parent process, youngest child, younger sibling,
     * older sibling, respectively. (p->father can be replaced with
     * p->real_parent->pid)
    */
    /* Real parent process: */
    struct task_struct __rcu *real_parent; //指向真实的父进程
    /* Recipient of SIGCHLD, wait4() reports: */
    struct task_struct __rcu *parent; //指向父进程。如果进程被另一个进程使用ptrace()跟踪,那么父进程跟踪进程,否则和real_parent相同
    struct task_struct *group_leader; //指向线程组的组长
    struct pid_link pids[PIDTYPE_MAX]; //进程号、进程组标识符和会话标识符
    char comm[TASK_COMM_LEN]; //进程名称
    /* Filesystem information: */
    struct fs_struct *fs; //文件系统信息,主要是进程的根目录和当前工作目录
    /* Open file information: */
    struct files_struct *files; //打开文件表
    /* Namespaces: */
    struct nsproxy *nsproxy; //命名空间
...
}

thread_info结构体

从上面的代码可以看出,task_struct结构体非常大。为了能用更少的寄存器就读取到进程描述符(task_struc),因此在内核栈底部增加了thread_info,并且把task_struct的指针存放在thread_info的第一个偏移位置,这样内核只需要通过内核栈就能访问到task_struct了。
对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:一个是内核态的进程堆栈stack,另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。这两个结构被紧凑的放在一个联合体中thread_union中。

union thread_union
{
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

在这里插入图片描述
thread_info和内核栈虽然共用了thread_union结构, 但是thread_info大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info的存储空间时, 则会发生栈溢出。
具体thread_info结构

struct thread_info {
	struct task_struct	*task;		/* main task structure */
	__u32			flags;		/* low level flags */
	__u32			status;		/* thread synchronous flags */
	__u32			cpu;		/* current CPU */
	mm_segment_t		addr_limit;
	unsigned int		sig_on_uaccess_error:1;
	unsigned int		uaccess_err:1;	/* uaccess failed */
};

可以看出,thread_info与task_struct结构体可以相互索引。

创建新进程

在Linux内核中,新进程是从一个已经存在的进程复制出来的,内核使用静态数据结构造出0号内核线程,0号内核线程分叉生成1号内核线程和2号内核线程(kthreadd线程)。1号内核线程完成初始化以后装载用户程序,变成1号进程,其他进程都是1号进程或者它的子孙进程分叉生成的;其他内核线程是kthreadd线程分叉生成的。

创建新进程的3个函数

目前,linux系统中有三个函数可以用来创建新的进程:

  1. fork:子进程是父进程的一个副本,采用定时复制技术。
  2. vfork:用于创建子进程,之后子进程立即调用execve以装载新程序的情况,为了避免复制物理页,父进程会睡眠等待子进程装载新程序。现在fork采用了定时复制技术,vfork失去了速度优势,已经被废弃。
    3.clone(克隆):可以精确地控制子进程和父进程共享哪些资源。这个系统调用的主要用处是可供pthread库用来创建线程。clone是功能最齐全的函数,参数多使用复杂,fork是clone的简化函数。

接下来,这三个函数会分别调用各自的系统调用函数,名称为"sys_"再加上各自的函数名,创建新进程的3个系统调用在文件kernel/fork.c中,最后,它们都会调用_do_fork()。

_do_fork()函数

在5.0版本后,linux不再使用该函数,而是改为了kernel_clone(),基本流程没有改变,明白了_do_fork,理解kernel_clone也就不难了。
贴出源码,主要是介绍流程。

long _do_fork(unsigned long clone_flags, //克隆标志
             unsigned long stack_start, //只有创建线程时有用,用来指定线程的用户栈起始地址
             unsigned long stack_size, //只有创建线程时有用,用来指定线程的用户栈长度
             int __user *parent_tidptr, //只有创建线程时有用,如果clone_flag指定CLONE_PARENT_SETID,该参数存放新线程保存自己的进程标识符的位置
             int __user *child_tidptr, //只有创建线程时有用,如果clone_flag指定CLONE_CHILD_SETID,该参数存放新线程保存自己的进程标识符的位置
             unsigned long tls) //只有创建线程时有用,如果clone_flag指定标志位CLONE_SETTLS,那么参数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;
    }
    // _do_fork()核心,创建新线程
    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;
}

如果有兴趣,可以对比一下,kernel_clone()的代码与此大体相似。
这里有一张_do_clone()的流程图。
在这里插入图片描述

copy_process()函数

这里提示一下,同一个线程组的所有线程必须属于相同的用户命名空间和进程号命名空间。

static __latent_entropy struct task_struct *copy_process(
    unsigned long clone_flags, 
    unsigned long stack_start,
    unsigned long stack_size,
    int __user *child_tidptr,
    struct pid *pid,
    int trace,
    unsigned long tls,
    int node)
{
    int retval;
    struct task_struct *p;
    //同时设置CLONE_NEWNS、CLONE_FS,即新进程属于新的挂载命名空间,同时和当前进程共享文件系统信息
    if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
        return ERR_PTR(-EINVAL);
    //新进程属于新的命名空间,同时和当前进程共享文件信息
    if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
        return ERR_PTR(-EINVAL);
    /*
     * Thread groups must share signals as well, and detached threads
     * can only be started up within the thread group.
    */
    //新进程和当前进程属于同一个线程组,但是不共享信号处理程序
    if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);
    /*
     * Shared signal handlers imply shared VM. By way of the above,
     * thread groups also imply shared VM. Blocking this case allows
     * for various simplifications in other code.
    */
    //新进程和当前进程共享信号处理程序,但是它不共享虚拟内存
    if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
        return ERR_PTR(-EINVAL);
...
    p = dup_task_struct(current, node);
    if (!p)
        goto fork_out;
...
    // 检查用户的进程数量限制
    if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) {
        if (p->real_cred->user != INIT_USER && !capable(CAP_SYS_RESOURCE) &  !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
        }
        current->flags &= ~PF_NPROC_EXCEEDED;
        retval = copy_creds(p, clone_flags); //复制证书
...
    // 检查用户的线程数量限制
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;
...
    // 为新进程设置调度器相关参数
    retval = sched_fork(clone_flags, p);
    
    /* copy all the process information */
    // 复制或共享资源
    shm_init_task(p);
    retval = security_task_alloc(p, clone_flags);
    if (retval)
        goto bad_fork_cleanup_audit;
    retval = copy_semundo(clone_flags, p); //只有属于同一个线程组的线程之间才会共享unix系统打开文件表,只有属于同一个线程组的线程之间才会共享打开文件表
    if (retval)
        goto bad_fork_cleanup_security;
    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); //创建或共享io上下文
    if (retval)
        goto bad_fork_cleanup_namespaces;
    retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls); //复制寄存器值。不同处理器架构的寄存器不同,所以各处理器架构需要自己定义结构体pt_regs和thread_struct来实现函数copy_thread_tls()
    if (retval)
        goto bad_fork_cleanup_io;
...
}

同样贴出流程图。
在这里插入图片描述

写时复制

在创建子进程时,linux采用的是写时复制技术。
在这里插入图片描述
写时复制核心思想:只有在不得不复制数据内容时才去复制数据内容。
具体解释一下,fork创建出的子进程,与父进程共享内存空间。也就是说,如果父进程或子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了(不用复制,直接引用父进程的物理空间)。事实上,虚拟空间两者不一样,但是子进程完全复制父进程的虚拟空间;而在物理空间上,两者是指向相同的地址的。

这里是刚开始对内核有了个入门的了解,如果看了没有感觉不要担心,后期还会继续介绍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值