xml地图|网站地图|网站标签 [设为首页] [加入收藏]

软件专题

当前位置:美高梅游戏网站 > 软件专题 > Linux内核学习笔记(3)-- 进程的创建和终结

Linux内核学习笔记(3)-- 进程的创建和终结

来源:http://www.gd-chuangmei.com 作者:美高梅游戏网站 时间:2019-12-04 07:13

一、 进程创建:

一、进程与线程

  Unix 下的进程创建很特别,与许多其他操作系统不同,它分两步操作来创建和执行进程: fork() 和 exec() 。首先,fork() 通过拷贝当前进程创建一个子进程;然后,exec() 函数负责读取可执行文件并将其载入地址空间开始运行。

    进程是处于执行期的程序,但是并不仅仅局限于一段可执行程序代码。通常,进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然还包括用来存放全局变量的数据段等。在Linux内核中,进程也通常叫做任务

1、fork() :kernel/fork.c

    执行线程,简称线程,是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的UNIX系统中,一个进程只包含一个线程,但在现在的系统中,包含多个线程的多线程程序司空见惯。在Linux系统中,线程和进程并不特别区分,对Linux而言,线程是一种特殊的进程

  在Linux系统中,通过调用fork()来创建一个进程。调用 fork() 的进程称为父进程,新产生的进程称为子进程。在该调用结束时,在返回点这个相同的位子上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次返回到父进程,另一次返回到新产生的子进程。使用fork()创建新进程的流程如下:

    Linux实现线程的机制很独特。从内核角度来说,它并没有线程这个概念。Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct ,所以在内核中,它看起来就像是一个普通的进程。

  1)fork() 调用clone;

 

  2)clone() 调用 do_fork();

二、进程描述符及任务结构

  3)do_fork() 调用 copy_process() 函数,copy_process() 函数将完成第 4-11 步;

  1)进程描述符 

  4)调用 dup_task_struct() 为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同;

    内核把进程的列表存放在任务队列中,任务队列是一个双向循环链表如图1所示。链表中每一项都是类型为 task_struct 的结构体,被称为 进程描述符,该结构定义在 <linux/sched.h>文件中。进程描述符中包含一个具体进程的所有信息。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态以及其他信息。 

  5)检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制;

图片 1

  6)清理子进程进程描述符中的一些成员(清零或初始化,如PID),以使得子进程与父进程区别开来;

图1 进程描述符及任务队列

  7)将子进程的状态设置为 TASK_UNINTERRUPTIBLE,保证它不会投入运行;

    Linux通过slab分配器分配 task_struct 结构,这样能达到对象复用和缓存着色的目的,为了找到 task_struct,只需在栈底(对于向下增长的栈)或栈顶(对于向上增长的栈)创建一个新的结构 struct thread_info,该结构存放着指向任务实际 task_struct 的指针。结构的定义如下:

  8)调用 copy_flags() 以更新 task_struct 的 flags 成员;

struct thread_info{
    struct task_struct     *task;
    struct exec_domain     *exec_domain;
    _u32                   flags;
    _u32                   status;
    _u32                   cpu;
    int                    preempt_count;
    mm_segment_t           addr_limit;
    struct restart_block   restart_block;
    void                   *sysenter_return;
    int                    uaccess_err;
};

  9)调用 alloc_pid() 为新进程分配一个有效的 PID;

2)进程状态    

10)根据传递给clone() 的参数标志,copy_process() 拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等;

    进程描述符中的 state 域描述了进程的当前状态。系统中进程的状态大致有以下这几种:

11)做一些扫尾工作并返回一个指向子进程的指针。

TASK_RUNNING(运行) 表示进程正在执行,或者在运行队列中等待执行;
TASK_INTERRUPTIBLE(可中断)

表示进程正在睡眠(被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行;

 TASK_UNINTERRUPTIBLE(不可中断) 除了就算接收到信号也不会被唤醒或者准备投入运行外,这个状态与可中断状态相同。这个状态通常在进程必须等待时不受干扰或者等待事件很快就会发生时出现;
__TASK_TRACED 被其他进程跟踪的进程;
 __TASK_STOPPED(停止)

进程停止执行,进程没有投入运行也不能投入运行。通常,这种状态发生在接收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

EXIT_ZOMBIE(僵死状态)

进程已经退出,但是进程本身所占的内存还没有被释放,如进程描述符等结构还保留着,以便父进程能够获得其停止运行的信息。当父进程获得需要的信息或者通知内核剩余的信息没用时,进程所占有的剩余的资源将被释放

EXIT_DEAD(死亡状态) 进程所占用的所有资源完全被释放

12)回到 do_fork() 函数,如果 copy_process() 函数成功返回,新创建的子进程将被唤醒并让其投入运行。

可以使用 set_task_state(task,state) 函数来设置当前进程状态:

  下面用一段简单的代码演示一下 fork() 函数:

set_task_state(task,state);        // 将进程task的状态设置为 state
  1 #include <unistd.h>
  2 #include <stdio.h>
  3 
  4 int main(){
  5     pid_t fpid;
  6     int count= 0;
  7     fpid = fork();              // fpid 为fork()的返回值
  8     if(fpid < 0){               // 当fork()的返回值为负值时,表明调用 fork() 出错
  9         printf("error in fork!");
 10     }
 11     else if(fpid  == 0){        // fork() 返回值为0,表明该进程是子进程
 12         printf("this is a child process, the process id is %dn",getpid());
 13         count++;
 14     }
 15     else{                       // fork() 返回值大于0,表明该进程是父进程,这时返回值其实是子进程的PID
 16         printf("this is a father process, the process id is %dn",getpid());
 17         count++;
 18     }
 19     printf("计数 %d 次n",count);
 20     return 0;                                                                          
 21 }

 

输出结果:

三、进程创建

图片 2

    linux使用 fork() 和 exec() 函数来创建进程。首先,使用 fork()函数拷贝当前进程创建一个子进程,这个子进程与父进程之间的区别仅在于 PID、PPID 以及某些资源统计量不同;然后调用 exec() 函数,把当前进程映像替换成新的进程文件,得到一个新程序。

  可以看到,调用 fork() 函数后,原本只有一个进程,变成了两个进程。这两个进程除了 fpid 的值不同外几乎完全相同,它们都继续执行接下来的程序。由于 fpid 的值不同,因此会进入不同的判断语句,这也是为什么两个结果有不同之处的原因。另外,可以看到,父进程的 PID 刚好比子进程的 PID 小1。 fork()  的返回值有以下三种:

    传统的 fork() 系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单且效率低下,因为它拷贝的数据也许并不共享。Linux 的 fork() 使用写时拷贝页实现,写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才会进行,在此之前,只是以只读的方式共享。这种技术使得地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下,它们就无须复制了。

a)在父进程中,fork() 返回新创建子进程的 PID;

 

b)在子进程中,fork() 返回0;

四、进程终结

c)如果 fork() 调用出错,则返回负值

     调用 do_exit() 来终结进程。当一个进程被终结时,内核必须释放它所占有的资源,并告知其父进程。

 

     在调用 do_exit() 之后,尽管线程已经僵死不能再运行了,但是系统还是保留了它的进程描述符。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的 task_struct 结构才被释放。调用 release_task() 来释放进程描述符。

 2、exec() :fs/exec.c (源程序 exec.c 实现对二进制可执行文件和 shell 脚本文件的加载与执行)

  通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用 exec() 这组函数就可以创建新的地址空间,并把新的程序载入其中

  exec() 并不是一个函数,而是一个函数簇,一共包含六个函数,分别为: execl、execlp、execle、execv、execvp、execve,定义如下:

本文由美高梅游戏网站发布于软件专题,转载请注明出处:Linux内核学习笔记(3)-- 进程的创建和终结

关键词: