进程的始与终

内核的调度对象是线程;资源分配的单位是进程。

一、进程控制块(PCB)

  • 进程控制块主要有以下结构体组成
/*include/linux/sched.h*/
struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below */

    int prio, static_prio, normal_prio;
    unsigned int rt_priority;

    struct list_head tasks;
    struct mm_struct *mm, *active_mm;

/* task state */
    int exit_state;
    int exit_code, exit_signal;
    int pdeath_signal;  /*  The signal sent when the parent dies  */

    /* Revert to default priority/policy when forking */
    unsigned sched_reset_on_fork:1;
    unsigned sched_contributes_to_load:1;

    unsigned long atomic_flags; /* Flags needing atomic access. */

    pid_t pid;
    pid_t tgid;
    /*
     * children/sibling forms the list of my natural children
     */
    struct list_head children;  /* list of my children */
    struct list_head sibling;   /* linkage in my parent's children list */
    struct task_struct *group_leader;   /* threadgroup leader */

    /* PID/PID hash table linkage. */
    struct pid_link pids[PIDTYPE_MAX];
    struct list_head thread_group;
    struct list_head thread_node;

    struct completion *vfork_done;      /* for vfork() */
/* CPU-specific state of this task */
    struct thread_struct thread;
/* filesystem information */
    struct fs_struct *fs;
/* open file information */
    struct files_struct *files;
/* signal handlers */
    struct signal_struct *signal;
}
  • 这个结构体有很多参数,我们要将其全部记住是不可能的,而且也没这个必要,我们可以从一个进程的角度去看待这个结构体,只要把握关键几个就可以了
  • 一个进程有基本的内存资源,描述自己当前文件系统,以及自己所打开的文件,还有基本的信号消息,以及记录父进程和子进程的相关结构体
    这里写图片描述

二、进程与线程

  • 进程与线程的差别主要在于,进程有独立的资源,而线程将会与父进程共享地址空间、文件系统、文件描述符和信号处理程序等资源

我们从函数调用入手,来理解这两者的区别

  • 线程的创建一般调用pthread_creat(),在源码中是调用 do_fork(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0)
  • 进程一般调用fork(),源码中调用do_fork(SIGCHLD , 0)
  • 有时候进程也会调用vfork(),源码中为do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD , 0)
  • 在后面会对这几个函数进行详细解析
参数标志 含义
CLONE_FILES 父子进程共享打开的文件
CLONE_FS 父子进程共享文件系统信息
CLONE_SIGHAND 父子进程共享信号处理函数及被阻断的信号
CLONE_VM 父子进程共享地址空间
CLONE_IDLETASK 将PID设置为0(只供idle进程使用)
CLONE_NEWNS 为子进程创建新的命名空间
CLONE_PARENT 指定子进程和父进程拥有同一个父进程
CLONE_PTRACE 继续调试子进程
CLONE_SETTID 将TID回写至用户空间
CLONE_SETTLS 为子进程创建新的TLS
CLONE_THREAD 父子进程放入相同的线程组
CLONE_SYSVSEM 父子进程共享System V SEM_UNDO语义
CLONE_VFORK 调用vfork(),所以父进程准备睡眠等待子进程将其唤醒

三、进程的始与终

  • 所有的进程都是PID为1的init进程的后代,然后通过fork()函数来创建子进程
  • 1、首先调用fork()函数创建新的进程,fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。
  • 2、调用exec()函数族创建新的地址空间,并把新的程序载入其中。
  • 3、调用exit()函数,释放进程相关资源,把进程状态设置为EXIT_ZOMBIE,即僵尸状态。
  • 4、父进程调用wait()函数,让子进程退出僵尸态,若不调用,则子进程一直处于僵尸态(浪费资源),直到父进程死亡。
int main(void)
{
    pid = fork();
    do {
        wait_pid=waitpid(pid, &status, WUNTRACED | WCONTINUED);

        if (wait_pid == -1) {
            perror("cannot using waitpid function");
            exit(1);
        }
        ……
        if(WIFSIGNALED(status))
            printf("child process is killed by signal %d\n", WTERMSIG(status));
        ……
    } while (!WIFEXITED(status) && !WIFSIGNALED(status));

    exit(0);
}

父进程可以通过wait()函数反馈回来的参数,判断子进程所处的状态,这也是僵尸态存在的意义。

四、进程的开始—do_fork()

SYSCALL_DEFINE0(fork)
{
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}

SYSCALL_DEFINE0(vfork)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
            0, NULL, NULL);
}
  • fork()、vfork()、pthread_creat()最终都会调用do_fork()函数,这里我们就讲解do_fork 函数
long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    ……
    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);

    if (!IS_ERR(p)) {
        ……
    } else {
        ……
    }
    return nr;
}
  • do_fork()函数主要调用copy_process()函数
static 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)
{
    ……

    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    ……

    /* Perform scheduler related setup. Assign this task to a CPU. */
    retval = sched_fork(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_policy;

    retval = perf_event_init_task(p);
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = audit_alloc(p);
    if (retval)
        goto bad_fork_cleanup_perf;
    /* 拷贝父进程的信息 */
    shm_init_task(p);
    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);//拷贝IO口相关信息
    if (retval)
        goto bad_fork_cleanup_namespaces;
    retval = copy_thread(clone_flags, stack_start, stack_size, p);//拷贝线程
    if (retval)
        goto bad_fork_cleanup_io;

    if (pid != &init_struct_pid) {
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);//分配PID
        if (IS_ERR(pid)) {
            retval = PTR_ERR(pid);
            goto bad_fork_cleanup_io;
        }
    }

    ……
    return p;
}
  • copy_process()函数中通过调用dup_task_struct()函数,将父进程的内核栈、thread_info结构和task_struct拷贝给子进程
  • vfork()与fork()相比,vfork()多了CLONE_VFORK和CLONE_VM这两个标志;父进程调用vfork()时,会被阻塞,直到子进程退出或执行exec()。

五、进程的结束—do_exit()

  • 当进程调用exit()返回时,进程处于僵尸态,基本已停止运行。
SYSCALL_DEFINE1(exit, int, error_code)
{
    do_exit((error_code&0xff)<<8);
}
  • exit()主要调用的是do_exit()这个函数
void do_exit(long code)
{
    ……
    profile_task_exit(tsk);

    set_fs(USER_DS);

    ptrace_event(PTRACE_EVENT_EXIT, code);

    validate_creds_for_do_exit(tsk);

    exit_signals(tsk);  /* sets PF_EXITING */

    tsk->exit_code = code;
    taskstats_exit(tsk, group_dead);

    exit_mm(tsk);//释放mm资源
    trace_sched_process_exit(tsk);

    exit_sem(tsk);//释放sem资源
    exit_shm(tsk);//释放shm资源
    exit_files(tsk);//递减文件描述符
    exit_fs(tsk);//退出文件系统
    exit_task_namespaces(tsk);
    exit_task_work(tsk);
    exit_thread();

    perf_event_exit_task(tsk);

    cgroup_exit(tsk);

    exit_notify(tsk, group_dead);//向父进程发送消息,将子进程设置为僵尸态
    proc_exit_connector(tsk);

    validate_creds_for_do_exit(tsk);

    exit_rcu();

    schedule();//切换到新的进程
    ……
}
  • 运行完do_exit()之后,尽管进程已经僵死不能再运行,但是系统还保留了它的内核栈、thread_info结构和task_struct结构。需要父进程调用wait()函数才能释放这些资源。

六、写时拷贝

  • 当父进程创建子进程时,子进程的所有结构体信息都会指向父进程,这时候父子进程会被设置为只读模式,当哪个先进行写操作,系统就会先为它分配对应的物理地址。如下图所示
    这里写图片描述

本文主要对宋宝华相关课程的总结,以及参考了《Linux内核设计与实现》

猜你喜欢

转载自blog.csdn.net/Smile_Smilling/article/details/78117492
今日推荐