精华内容
下载资源
问答
  • 进程创建1.1 fork1.2 vfork2. 进程终止3. 进程等待4. 进程程序替换 1. 进程创建 1.1 fork 在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个新进程。而这个新创建出来的进程被称为原进程的子进程,原...

    1. 进程创建

    1.1 fork

    在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个新进程。而这个新创建出来的进程被称为原进程的子进程,原进程被称为该进程的父进程。

    该函数其实是一个系统调用接口,原型如下:

    #include <unistd.h>
    pid_t fork(void);
    

    特性:子进程会复制父进程的PCB,二者之间代码共享,数据独有,拥有各自的进程虚拟地址空间

    此时可能会有一个疑问,既然代码共享,并且子进程是拷贝了父进程的PCB,虽然他们各自拥有自己的进程虚拟地址空间,但其中的数据必然是相同的(拷贝而来),并且通过页表映射到同一块物理内存中,那么又如何做到数据独有呢?答案是:通过写时拷贝技术。

    写时拷贝技术:子进程创建出来后,与父进程映射访问同一块物理内存,但当父子进程当中有任意一个进程更改了内存中的数据时,会给子进程重新在物理内存中开辟一块空间,并将数据拷贝过去。 这样避免了直接给子进程重新开辟内存空间,造成内存数据冗余。换句话说,如果父子进程都不更改内存中的值,那他们二者各自的进程虚拟地址空间通过页表映射,始终是指向同一块物理内存。

    正是通过这样的写时拷贝技术,才保证了父子进程代码共享但数据独有的这一特性。 对于一些小萌新来说,可能上述文字描述并不是那么直观,此处有必要上图来进一步说明一下:

    如父进程中有全局变量g_val初值为10,子进程创建之后通过复制父进程的PCB,并且二者的进程虚拟地址空间通过页表映射到同一块物理内存,但如果子进程更改了g_val的值,就会在物理内存中开辟新的空间并保存属于子进程的g_val:
    在这里插入图片描述

    在知道了以上特性后,下面我们来认识一下fork函数的返回值,相当重要!

    通过以上函数原型我们可以看到起返回值是pid_t类型,其实就是int,在内核中是通过typedef重命名过的,我们把其当做int类型即可。

    如果创建子进程失败,会返回-1,是小于0的,而如果创建子进程成功,该函数则会返回俩个值,这一点和普通的函数有很大区别。它会给子进程返回0值,而给父进程返回子进程的pid(一个大于0的数),也正是通过给父子进程返回值的不同,从而我们可以使用选择语句对齐进行分流,从而让父子进程执行不同的代码,而达到我们创建子进程的某种目的。

    在了解到这一点之后,我们便可以通过代码来创建子进程并且进一步验证前面说到的一些特性。

    #include <stdio.h>
    #include <unistd.h>
    
    //父子进程代码共享,但数据独有
    int g_val = 100;
    int main()
    {
        pid_t pid = fork();//创建子进程
        if(pid < 0) {
            printf("fork error!\n");
            return -1; 
        }
        else if(pid == 0) {
            //子进程
            g_val = 200;
            printf("This is Child! g_val = %d p = %p\n",g_val,&g_val);
        }
        else {
            //父进程
            sleep(1);
            printf("This is Parent! g_val = %d p = %p\n",g_val,&g_val);
        }
        return 0;
    }
    

    运行程序,得到如下结果:
    在这里插入图片描述
    对于这一结果感到惊讶吗?其实只要你看懂了我上面所说的内容,相信这个结果并不难理解:子进程拷贝父进程的PCB,拥有和父进程一模一样的进程虚拟地空间以及数据,但子进程将自己的g_val更改后,会在物理内存中为其重新开辟空间来存储子进程更改后的数据,而结果中看到的地址完全相同,则是因为它们仅仅是虚拟的地址空间,真正的值是存储在物理内存中的。而这时通过页表的映射,这俩个看似相同的地址已经指向了不同的物理内存。

    1.2 vfork

    不止可以通过fork来创建子进程,vfork也同样是用来创建子进程的系统调用函数,那么它和fork有什么区别呢?

    #include <sys/types.h>
    #include <unistd.h>
    pid_t vfork(void);
    

    通过函数原型我们似乎并不能看出什么端倪,的确,vfork在使用时和fork几乎没有什么区别,返回值及其含义也和fork完全相同。其和fork的区别在于,用v_fork创建出来的子进程,也是拷贝父进程的PCB,但它和父进程共享同一个进程虚拟地址空间。也就是如下图所示的这样:
    在这里插入图片描述
    但是我们要思考一个问题,父子进程共享同一个进程虚拟地址空间不会有问题吗?会的!会造成调用栈混乱的问题! 举个例子,如果父进程中调用Test函数首先压栈,之后子进程则调用Fun函数,由于二者共享同一个栈空间,则Fun函数也会继续压栈,但如果此时父进程的Test函数调用完毕想要返回,却发现其并不在栈顶位置,无法出栈,这不就有问题了吗?
    在这里插入图片描述

    那怎么解决呢?vfork采用的方案是,在其创建出子进程之后,让子进程先执行,而父进程则会阻塞,直到子进程执行完毕,父进程才会开始执行,这样就避免了调用栈混乱的问题。

    但是!这个问题是解决了,可是新的问题也随之而来了呀,我们创建子进程难道不是为了让其而父进程并发的跑或者说更高效的完成一些任务吗,而现在再子进程退出前父进程什么都不能做,这难道不会影响效率吗?或者说的再直白一些,不是浪费时间吗???

    不得不说,确实。可能也正是因为这些种种的缺点,vfork这个函数已然逐渐的被时代淘汰了,fork它不香吗?为什么要用vfork呢? 博主也理解不了它存在的意义…不过也罢,我们只需稍作了解,然后还是把爱全都给fork吧!
    在这里插入图片描述

    2. 进程终止

    含义:进程终止的含义就是一个进程的退出。

    进程退出的场景

    1. 程序运行完毕,从main函数中退出
      1.1 运行完毕,结果正确
      1.2 运行完毕,结果不正确
    2. 程序没有运行完毕,中途奔溃了

    进程常见退出方法:

    1. 正常退出:

    1. 从main函数返回
    2. 调用exit函数
    3. 调用_exit函数

    2. 异常退出: Ctrl+C,信号终止等

    exit函数:

    #include <stdlib.h>
    void exit(int status);
    

    其中,stauts定义了进程的终止状态,由用户自己传递,父进程可以通过wait来获取该值(下边进程等待部分实操)。

    _exit函数:

    #include <unistd.h>
    void _exit(int status);
    

    exit和_exit俩个函数都可以退出当前进程,而二者的区别在于:exit是库函数,_exit是系统调用函数,而库函数内部封装了系统调用。 也就是说,调用exit函数最终也会调用_exit来使进程退出,只不过在其调用_exit之前,还会做一些其他的事情,如下图:
    在这里插入图片描述
    从上图我们可以看出,exit()与_exit()还有一个很重要的区别就是在退出前会不会刷新缓冲区。显然,前者是会刷新缓冲区的,这也是它在封装后者的基础上所增加了一些后者并不具备的功能。

    代码验证如下:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
      printf("我要退出了!\n");
      exit(1);
      printf("应该不会打印我了!\n");
      return 0;
    }
    

    运行以上代码,结果如下:
    在这里插入图片描述
    以上结果符合完全符合我们的预期,那如果使用_exit()呢?我们再试试:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
      printf("我要退出了!");
      _exit(1);
      printf("应该不会打印我了!\n");
      return 0;
    }
    

    在这里插入图片描述
    不是说_exit()退出时不会刷新缓冲区吗?怎么还是会打印出来呢?注意:不是bug,原因是\n(换行符)也有刷新缓冲区的作用。我们去掉\n再次执行代码就会看到我们预期的结果:
    在这里插入图片描述
    那如何说明一开始调用exit不是因为其内部会刷新缓冲区而不是\n的作用呢?很简单,去掉\n再试试就清楚了,肯定也是会刷新缓冲区而打印对应内容的,只不过不会换行了。这里就不在演示。

    再补充一点,除了\n(换行)以及exit()函数会刷新缓冲区之外,也可以调用fflush()来强制刷新缓冲区

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
      printf("我要退出了!");//没有\n
      fflush(NULL);//刷新缓冲区
      _exit(1);
      printf("应该不会打印我了!\n");
      return 0;
    }
    

    在这里插入图片描述
    以上结果与使用exit函数退出进程并且前一条打印语句不带\n一致,大家可以自行验证。

    3. 进程等待

    3.1 为什么要进程等待

    之前在了解进程概念的的时候有说到过僵尸进程,如果子进程先于父进程退出,而父进程并没有关心子进程的退出状况,从而无法回收子进程的资源,就会导致子进程变成僵尸进程。

    如果对信号有一定的了解,就会知道,僵尸进程一旦产生就算是kill-9这样的强杀信号都杀不掉它,因为谁也没办法杀掉一个已经死去的进程! 那怎么办呢?当时在进程概念的位置并没有提解决(避免)僵尸进程的办法,而在这个位置再次说到它,就是想来引出进程等待这个概念。进程等待的作用就是防止僵尸进程的产生!

    进程等待:父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出状态。

    那具体如何完成进程等待呢?答:在父进程中,使用wait或waitpid接口来完成进程等待。

    3.2 wait

    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
    

    返回值:成功会返回被等待进程的pid,失败则会返回-1

    参数:一级指针status,它其实是个输出型参数,用于获取子进程的退出状态,如果不关心则可以设置为NULL

    代码实例:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
      pid_t pid = fork(); //创建子进程
      if(pid < 0) {
        perror("fork");
        return -1;
      }
      else if(pid == 0) {
        //子进程
        printf("I am child, my pid is %p\n", getpid());
        sleep(3);
      }
      else {
        //父进程
        printf("I am father, my pid is %p\n", getpid());
        wait(NULL); //进程等待
        printf("进程等待成功!\n");
      }
      return 0;
    }
    

    在这里插入图片描述
    执行以上程序,等够成功等待使我们预期之内的,但我们还应知道的一点是,wait是一个阻塞接口,意味着它在等待子进程退出期间是阻塞在函数内部的,直到子进程退出,它获取了子进程的退出状态并回收子进程的资源,才会返回。 如果要验证以上结论,可以适当增加子进程中休眠的时间,然后使用pstack[父进程进程号] 查看调用堆栈就可以看出,这里不再进行验证。

    3.3 waitpid

    //头文件同wait的头文件
    pid_t waitpid(pid_t pid, int *status, int options);
    

    waitpid同样也可以被用来进行进程等待,但它较wait接口稍稍复杂一些:

    返回值:

    1. 等待成功正常返回则返回被等待进程的pid
    2. 如果第三个参数options设置成了WNOHANG,而此时没有子进程退出(没有成功等待到子进程),就会返回0,而不是阻塞在函数内部
    3. 调用出错则返回-1

    参数:

    1. pid,设置成-1则表示等待任意一个子进程,同wait;如果>0则表示等待一个指定的子进程,pid就是被等待子进程的进程号
    2. status,出参,获取子进程的退出状态,同wait
    3. options,可以设置为0或WNOHANG。设置为0则与wait一样,如果没有等待到子进程退出会一直阻塞;而设置为WNOHANG则表示非阻塞,如果被等待的子进程未退出,则会返回0值,成功等待到子进程则会返回被等待子进程的pid

    也就是说,如果使用waitpid接口并设置options参数为WNOHANG,则未等待到子进程退出时也会立即返回,而不是阻塞,因此这种场景我们一般搭配循环来使用,以确保可以成功等待到子进程退出。

    代码实例:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
      pid_t pid = fork();
      if(pid < 0) {
        perror("fork");
        return -1;
      }
      else if(pid == 0) {
        //子进程
        printf("I am child, pid is %p\n", getpid());
        sleep(10);
      }
      else {
        printf("I am father, pid is %p\n", getpid());
        while(waitpid(pid, NULL, WNOHANG) == 0); //循环调用waitpid,直到其返回值不为0
        printf("进程等待成功!\n");
      }
      return 0;
    }
    

    运行结果:10秒之后waitpid成功等待到子进程退出而返回非0值,跳出while循环并执行后续打印语句
    在这里插入图片描述
    其他传参方式大家可以自行验证。

    3.4 获取子进程退出信息status

    我们发现,不论是wait还是waitpid都有一个出参status,而我们之前并未关心这一点,那么这里就来探讨一下如何获取子进程的退出状态吧!

    之前,我们已经知道status是一个出参,由操作系统为其赋值,用户可以传递NULL值表示不关心,而如果传入参数,操作系统就会根据该参数,将子进程的退出信息反馈给父进程,由status最终被赋予的值来体现。

    那么,到底如何通过status来获取子进程的退出信息呢,要知道这一点,我们必须先知道status的使用细节:

    status是一个int类型的值,意味着它应该有32个比特位,但它又不能被当初普通的整形来看待,因为其高16位的值并不被使用,而只使用其低16个比特位:
    在这里插入图片描述
    那么,在只关心其低16位的基础上,具体的比特位又分别代表什么含义呢,也就是如何通过这低16个比特位来获取子进程的退出信息呢,我们同样通过俩张图来解释:

    子进程正常退出时:
    在这里插入图片描述
    子进程异常退出时:
    在这里插入图片描述
    图片表达应该更加直观一些,不过还是要稍作解释:可以看出,不论是正常退出还是异常退出,status的高8个比特位(只讨论低16个比特位)都表示子进程的退出码,而这个退出码一般是return的返回值或者exit的参数;正常退出时,status的低8个比特位为全0;而异常退出时,其第8个比特位则为core dump标志位,用来标志是否会有core dump文件产生,而低7个比特位则是退出信号。

    我们可以分别通过以位运算的方式来分别得到以上信息:

    退出码:(status >> 8) & 0xFF

    低7位(检测子进程是否异常退出):status & 0x7F

    • 结果为0则表示正常退出
    • 不为0则说明是异常退出,因为有终止信号

    core dump标志位:(status >> 7) & 0x1

    • 结果为0则表示没有core dump产生
    • 等于1则说明有core dump产生

    通过代码来进一步验证以上结论:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
      pid_t pid = fork();
      if(pid < 0) {
        perror("fork");
        return -1;
      }
      else if(pid == 0) {
        //子进程
        printf("I am child, pid is %p\n", getpid());
        sleep(3);
        exit(20); //退出子进程并将其退出码设置为20
      }
      else {
        printf("I am father, pid is %p\n", getpid());
        int status; //定义status,让操作系统为其赋值
        waitpid(-1, &status, 0); //这种传参方式的waitpid和wait几乎没有区别
        printf("进程等待成功!\n");
        //低7位为全0则表示正常退出
        if((status & 0x7F) == 0) {
          printf("正常退出!\n");
          printf("exitcode = %d\n", (status >> 8) & 0xFF);
        }
        else {
          printf("异常提出!\n");
          printf("core dump flag = %d\n", (status >> 7) & 0x1);
        }
      }
      return 0;
    }
    

    运行结果:
    在这里插入图片描述

    4. 进程程序替换

    原理:进程程序替换其实是替换当前正在运行程序的代码段和数据段,并更新堆栈信息。

    有关进程虚拟地址空间以及根据页表映射至物理内存这一模式大家都已经非常熟悉了,这里就不再画图解释。我们需要知道的是,进程程序替换与fork不同,它并不会创建新的进程,而是该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。替换前后的进程号并未改变。

    4.1 exec函数簇

    我们一般通过替换函数来完成进程程序替换,也就是exec函数簇,需要注意的是,它并不是一个函数,而是多个函数。他们都以exec开头,统称exec函数,其函数原型如下:

    #include <unistd.h>
    
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);
    int execle(const char *path, const char *arg,..., char * const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[],char *const envp[]);
    

    首先说返回值

    • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
    • 如果调用出错则返回-1
    • 所以exec函数只有出错的返回值而没有成功的返回值

    参数解释

    path/file:要替换的可执行程序的名称,path需要带路径
    arg/argv[]:可执行程序的参数,规定其第一个参数必须是可执行程序的名称,并以NULL结尾表示参数传递完毕,二者的区别在于参数是以可变参数列表还是字符数组的方式给出
    envp[]:程序员自己组织的环境变量,以数组的方式给出,内部同样需要以NULL结尾,如果传入NULL则认为当前程序没有环境变量

    如果以上描述还不是很好理解,那么我们可以再仔细观察下这些函数的区别,可以发现,除了开头都是exec这一共同点之外,其余字母无非就是l或v的区别、有没有p的区别以及有没有e的区别:

    l或v的区别

    • l表示命令行参数为可变参数列表,传参数时需要以NULL结尾
    • v表示命令行参数为指针数组,由程序员自己提供

    有没有p的区别:是否会去搜索环境变量

    • 有p则代表会去环境变量PATH去搜索当前要替换程序所在的位置
    • 没有p则意味着不会去搜索环境变量,需要程序员自己提供想要替换的程序所在的路径

    有没有e的区别:是否需要程序员自己组织环境变量

    • 没有e则表示不需要程序自己组织环境变量,内核会将当前的环境变量继承下来
    • 有e则代表需要程序员自己组织环境变量,如果直接传递NULL则表示当前程序没有环境变量;如果自己组织环境变量,则指针数组中得到环境变量需要以NULL结尾

    代码实战:

    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
        printf("下面进行进程替换!\n");
    
        //将当前程序替换为ls程序
        execl("/usr/bin/ls","ls","-l",NULL); //l表示命令行以可变参数列表的形式给出,没有p则说明需要带路径,没有e说明不需要自己组织环境变量
    
        //如果进程替换成功,下面的代码将不再执行
        printf("继承替换失败!\n");
    
        return 0;
    }
    

    在这里插入图片描述
    上述代码以execl为例简单的演示了进程程序替换的实际效果,也完全符合我们的预期,其他函数大家可以自己尝试,都非常的简单。

    还需要补充的一点就是:如果使用man去查看这些函数,会发现他们都在3号手册,也就是库函数所在的手册,意味着上述的exec函数簇其实本身都是库函数,而非系统调用。其实,不论是哪个函数,它们最终都会去调用一个叫做execve的系统调用函数,从而真正完成进程程序替换。

    #include <unistd.h>
    int execve(const char *filename, char *const argv[], char *const envp[]);
    

    不仅如此,而这些函数内部,其实也是相互调用的逻辑,不过最终都还是会去调用execve来完成进程程序替换:
    在这里插入图片描述
    文章到这里就结束了,如果感觉博主写的还行的话,就点个赞吧~

    展开全文
  • Linux——进程创建和进程终止

    千次阅读 2021-03-15 21:05:48
    进程创建&进程终止1.进程创建1.1fork函数初识1.2 fork函数返回值1.3 写时拷贝2.进程终止 1.进程创建 1.1fork函数初识 fork函数,从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程 进程调用...

    1.进程创建

    1.1fork函数初识

    • fork函数,从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程
      在这里插入图片描述
    • 进程调用fork,当控制转移到内核中的fork代码后,内核会做如下几件事:
    • 分配新的内存块和内核数据结构给子进程
    • 将父进程部分数据结构内容拷贝至子进程
    • 添加子进程到系统进程列表当中
    • fork返回,开始调度器调度

    1.2 fork函数返回值(为什么两个返回值?)

    在这里插入图片描述

    1.3 写时拷贝(代码共享why和数据私有why)

    • 默认情况下,父子进程共享代码,数据各自私有一份
    • 代码共享:所有代码共享,一般都是从fork()函数之后开始执行,原因:代码不可以被修改,所以各自浪费私有空间
    • 数据私有原因:因为进程之间具有独立性,数据是很多的,但并不是所有的数据都立马要使用,且不是所有的数据都需要进行拷贝。如果需要立马独立,就要将数据全部拷贝,把本来可以再后面拷贝的,甚至不需要拷贝的数据全部拷贝,但是这样十分的浪费时间和空间
    • 拷贝的过程不是立马做的,父子代码共享,父子再不写入时,数据也是共享的,此时,如果任意一方试图写入,便以写时拷贝的方式各自一份副本,所以我们这里引入一个写时拷贝的概念
      在这里插入图片描述

    1.4 fork常规用法和调用失败的原因

    1.4.1 常规用法

    • 子进程赋值父进程,通过if、else来实现父子进程执行不同的代码段。比如父进程等待客户端请求生成子进程来进行处理
    • 一个进程要执行一个不同的程序。比如fork后,子进程调用exec函数

    1.4.2调用失败的原因

    • 系统中有太多的进程
    • 实际用户的进程数超过了限制

    1.5 如何理解子进程创建和fork()?

    • 子进程创建的实质是系统多了一个进程,因此OS需要将子进程也管理起来,就需要创建新的PCB,虚拟空间,页表,映射关系(这些属性自己进程都以父进程为模板拷贝过来)
    • 并且当父子进程各有一方想要写入数据时就会发生写时拷贝,将数据独立出来,比如说数据一共是10M,但写入的时候是1M,发生写时拷贝,此时拷贝的应该是1M的数据

    2.进程终止

    2.1进程退出场景

    在这里插入图片描述
    在这里插入图片描述

    2.2为什么main()函数的返回值通常是0呢?

    • 因为在C/C++中的函数设计通常把0表示为正确,当main()函数执行结束的时候返回0,表示这个函数正常运行完,并且结果正确,非0表示错误,每种退出码对应一种错误。

    2.3进程常见退出方法

    2.3.1正常终止

    • 从main函数返回
    • 调用库函数exit()
    • _exit(接口调用)

    2.3.2异常终止

    • ctrl+c,信号终止

    2.4 exit,_exit,return区别

    • exit:终止整个进程,任何地方调用,都会终止进程,会刷新缓冲区
      在这里插入图片描述
      在这里插入图片描述

    • _exit:属于系统调用,不会刷新缓冲区
      在这里插入图片描述
      在这里插入图片描述

    • return退出:return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数

    展开全文
  • 进程创建过程

    千次阅读 2019-02-25 16:07:26
    进程创建进程创建时,调用do_fork函数来创建新进程,那么和调度相关的操作主要有两个,一个是sched_fork,这是对一个进程进行调度的初始化,另外一个就是wake_up_new_task,这个是把刚刚创建的子进程唤醒加入到...

    进程创建

    在进程创建时,调用do_fork函数来创建新进程,那么和调度相关的操作主要有两个,一个是sched_fork,这是对一个进程进行调度的初始化,另外一个就是wake_up_new_task,这个是把刚刚创建的子进程唤醒加入到调度器中管理。
    首先来看sched_fork函数,调用流为do_fork–>copy_process–>sched_fork。

     /*
      * fork()/clone()-time setup:
      */
     int sched_fork(unsigned long clone_flags, struct task_struct *p)
     {
         unsigned long flags;
         int cpu = get_cpu();
     
         __sched_fork(clone_flags, p);   //调度相关结构体的初始化操作函数
         /*
          * We mark the process as running here. This guarantees that
          * nobody will actually run it, and a signal or other external
          * event cannot wake it up and insert it on the runqueue either.
          */
         p->state = TASK_RUNNING;       //设置进程状态为RUNNING
     
         /*
          * Make sure we do not leak PI boosting priority to the child.
          */
         p->prio = current->normal_prio;  //设置优先级为普通优先级
     
         /*
          * Revert to default priority/policy on fork if requested.
          */
         if (unlikely(p->sched_reset_on_fork)) {
             if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
                 p->policy = SCHED_NORMAL;
                 p->static_prio = NICE_TO_PRIO(0);
                 p->rt_priority = 0;
             } else if (PRIO_TO_NICE(p->static_prio) < 0)
                 p->static_prio = NICE_TO_PRIO(0);
     
             p->prio = p->normal_prio = __normal_prio(p);
             set_load_weight(p);
     
             /*
              * We don't need the reset flag anymore after the fork. It has
              * fulfilled its duty:
              */
             p->sched_reset_on_fork = 0;     //以上这段是为了判断是否要重置调度策略
         }
     
         if (dl_prio(p->prio)) {
             put_cpu();
             return -EAGAIN;
         } else if (rt_prio(p->prio)) {
             p->sched_class = &rt_sched_class;
         } else {
             p->sched_class = &fair_sched_class;  //普通进程设置调度类为CFS调度器
         }
     
         if (p->sched_class->task_fork)
             p->sched_class->task_fork(p);  //执行调度类中的task_fork回调
     
         /*
          * The child is not yet in the pid-hash so no cgroup attach races,
          * and the cgroup is pinned to this child due to cgroup_fork()
          * is ran before sched_fork().
          *
          * Silence PROVE_RCU.
          */
         raw_spin_lock_irqsave(&p->pi_lock, flags);
         set_task_cpu(p, cpu);            //设置子进程的cpu为父进程的cpu
         raw_spin_unlock_irqrestore(&p->pi_lock, flags);
     
     #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
         if (likely(sched_info_on()))
             memset(&p->sched_info, 0, sizeof(p->sched_info));
     #endif
     #if defined(CONFIG_SMP)
         p->on_cpu = 0;
     #endif
         init_task_preempt_count(p);    //进程抢占标志初始化
     #ifdef CONFIG_SMP
         plist_node_init(&p->pushable_tasks, MAX_PRIO);
         RB_CLEAR_NODE(&p->pushable_dl_tasks);
     #endif
     
         put_cpu();
         return 0;
     }
    
    

    下面来看CFS调度器中task_fork的实现,调用流do_fork–>copy_process–>sched_fork–>task_fork_fair:

    static void task_fork_fair(struct task_struct *p)
    {
        struct cfs_rq *cfs_rq;
        struct sched_entity *se = &p->se, *curr;
        int this_cpu = smp_processor_id();
        struct rq *rq = this_rq();
        unsigned long flags;
    
        raw_spin_lock_irqsave(&rq->lock, flags);
    
        update_rq_clock(rq);
    
        cfs_rq = task_cfs_rq(current);
        curr = cfs_rq->curr;
    
        /*
         * Not only the cpu but also the task_group of the parent might have
         * been changed after parent->se.parent,cfs_rq were copied to
         * child->se.parent,cfs_rq. So call __set_task_cpu() to make those
         * of child point to valid ones.
         */
        rcu_read_lock();
        __set_task_cpu(p, this_cpu);
        rcu_read_unlock();
    
        update_curr(cfs_rq);
    
        if (curr)
            se->vruntime = curr->vruntime;
        place_entity(cfs_rq, se, 1);
    
        if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
            /*
             * Upon rescheduling, sched_class::put_prev_task() will place
             * 'current' within the tree based on its new key value.
             */
            swap(curr->vruntime, se->vruntime);
            resched_curr(rq);
        }
    
        se->vruntime -= cfs_rq->min_vruntime;
    
        raw_spin_unlock_irqrestore(&rq->lock, flags);
    }
    

    这个函数主要实现的是如下几个步骤:
    (1)更新runqueue clock
    (2)设置当前进程cpu为父进程的CPU
    (3)update_curr是CFS调度器中核心函数,更新父进程的sum_exec_runtime,vruntime和runqueue的min_vruntime
    (4)place_entity对于新创建的进程进行惩罚,vruntime会加上一个值,放置新创建进程恶意占有CPU

    加入运行队列(enqueue操作)

    上面介绍的就是进程创建关于调度的初始化过程,那么初始化完成后,下面就要把新的子进程加入到调度器中,涉及的函数如下do_fork–>wake_up_new_task:

     void wake_up_new_task(struct task_struct *p)
     {
         unsigned long flags;
         struct rq *rq;
     
         raw_spin_lock_irqsave(&p->pi_lock, flags);
     #ifdef CONFIG_SMP
         /*
          * Fork balancing, do it here and not earlier because:
          *  - cpus_allowed can change in the fork path
          *  - any previously selected cpu might disappear through hotplug
          */
         set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));   //子进程重新选择runqueue和cpu,相当于进行了一次负载均衡处理
     #endif
     
         /* Initialize new task's runnable average */
         init_task_runnable_average(p);                   //依据权重初始化子进程的时间片和负载贡献
         rq = __task_rq_lock(p);
         activate_task(rq, p, 0);                         //把子进程加入到runqueue,这是该函数的关键核心
         p->on_rq = TASK_ON_RQ_QUEUED;
         trace_sched_wakeup_new(p, true);
         check_preempt_curr(rq, p, WF_FORK);
     #ifdef CONFIG_SMP
         if (p->sched_class->task_woken)
             p->sched_class->task_woken(rq, p);
     #endif
         task_rq_unlock(rq, p, &flags);
     }
    
    
    void activate_task(struct rq *rq, struct task_struct *p, int flags)
    {   
        if (task_contributes_to_load(p))
            rq->nr_uninterruptible--;
        
        enqueue_task(rq, p, flags);
    }
    
    static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
    {
        update_rq_clock(rq);
        sched_info_queued(rq, p);
        p->sched_class->enqueue_task(rq, p, flags);
    }   
    
    static void
    enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
    {
        struct cfs_rq *cfs_rq;
        struct sched_entity *se = &p->se;
    
        for_each_sched_entity(se) {
            if (se->on_rq)
                break;
            cfs_rq = cfs_rq_of(se);
            enqueue_entity(cfs_rq, se, flags);  //调度实体加入到runqueue中
    
            /*
             * end evaluation on encountering a throttled cfs_rq
             *
             * note: in the case of encountering a throttled cfs_rq we will
             * post the final h_nr_running increment below.
            */
            if (cfs_rq_throttled(cfs_rq))
                break;
            cfs_rq->h_nr_running++;
    
            flags = ENQUEUE_WAKEUP;
        }
    
        for_each_sched_entity(se) {
            cfs_rq = cfs_rq_of(se);
            cfs_rq->h_nr_running++;
    
            if (cfs_rq_throttled(cfs_rq))
                break;
    
            update_cfs_shares(cfs_rq);      //更新cfs shares
            update_entity_load_avg(se, 1);  //更新调度实体负载和runqueue负载,在实际上在上面的enqueue_entity也会执行这两步
        }
    
        if (!se) {
            update_rq_runnable_avg(rq, rq->nr_running);
            add_nr_running(rq, 1);
        }
        hrtick_update(rq);
    }
    
    static void
    enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
    {
        /*
         * Update the normalized vruntime before updating min_vruntime
         * through calling update_curr().
         */
        if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
            se->vruntime += cfs_rq->min_vruntime;  //新创建进程加上一个min_vruntime
    
        /*
         * Update run-time statistics of the 'current'.
         */
        update_curr(cfs_rq);  //更新子进程runqueue对应的当前进程相关的时间信息和vruntime信息
        enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);  //子进程加入到对应runqueue的负载计算中
        account_entity_enqueue(cfs_rq, se);
        update_cfs_shares(cfs_rq);
    
        if (flags & ENQUEUE_WAKEUP) {
            place_entity(cfs_rq, se, 0); //对于刚刚唤醒的进程进行补偿,vruntime减去一个值,提高优先级执行
            enqueue_sleeper(cfs_rq, se);
        }
    
        update_stats_enqueue(cfs_rq, se);
        check_spread(cfs_rq, se);
        if (se != cfs_rq->curr)
            __enqueue_entity(cfs_rq, se); //加入到runqueue中的rb tree的处理
        se->on_rq = 1;
    
        if (cfs_rq->nr_running == 1) {
            list_add_leaf_cfs_rq(cfs_rq);
            check_enqueue_throttle(cfs_rq);
        }
    }
    
    

    经历了以上这些操作以后,一个进程的创建到加入runqueue的过程就完成了,后续该进程就开始接受CFS调度器的调度了。

    展开全文
  • 进程创建与撤销实验目的实现总体设计代码实现 实验目的 模拟进程的创建、就绪、撤销,并按优先级对进程排序。 实现 总体设计 数据结构定义 结构体PCB: 进程名、ID、运行时间、优先级等,队列的排序按优先级排序。 ...

    实验目的

    模拟进程的创建、就绪、撤销,并按优先级对进程排序。

    实现

    总体设计

    1. 数据结构定义
      结构体PCB: 进程名、ID、运行时间、优先级等,队列的排序按优先级排序。
      PCB空间: 结构体PCB数组
      队列: 就绪队列,消息缓冲队列
    2. 函数
      Create()—进程创建: 从PCB空间申请一个空PCB,填入进程参数,插入就绪队列;
      kill()—进程终止: 将指定的就绪进程移出就绪队列,清楚PCB信息;
      display()-就绪队列输出: 输出就绪队列中的进程信息,以便观察创建或撤消活动的结果;
      Sort()—进程优先级排序: 根据你定义的优先级,把进程进行排序。
      menu()—功能选择 :调用创建函数;显示就绪队列;调用终止函数;退出程序等
    3. 功能测试
      从显示出的就绪队列状态,查看操作的正确与否。

    代码实现

    #include <stdio.h>
    #include <stdlib.h>
    
    ///pcb控制块信息
    typedef struct P_node{
        char t_name[20];///进程名
        long t_id;///进程id
        float t_time;///进程运行时间
        int priority;///进程优先级
    }PCB;
    
    ///就绪队列
    typedef struct Tnode *T;
    struct Tnode{
        int data;
        T next;///指向就绪队列的下一个位置
    };
    
    ///消息缓冲队列
    typedef struct Linkqueue{
        T front,rear;///头指针和尾指针分别指向就绪队列的头和尾
    }LQ;
    
    LQ L;
    PCB pcb[100];///结构体PCB数组,模拟当前内存大小
    int N=0;///当前已经创建的进程数目
    
    ///就绪队列初始化
    void InitQueue(){
        L.rear=L.front=malloc(sizeof(T));
        if(!L.front)
            exit(-2);
        L.front->next=NULL;
        return 0;
    }
    
    ///创建进程:申请PCB,填入进程参数
    void Create(){
        int n;///要创建的进程的数目
        printf("请输入要创建的进程的数目:");
        scanf("%d",&n);
        ///要申请的进程数大于当前系统的内存
        if(n>100){
            printf("当前内存已满");
            return 0;
        }
        int i,j;
        long id;
        T t;
            for(i=1;i<=n;i++){
                ///当就绪队列为空时,第一个进程id可以直接写入
                if(L.front->next==NULL){
                    printf("请输入进程id:");
                    scanf("%ld",&pcb[i+N].t_id);
                    getchar();
                    printf("请输入进程名:");
                    gets(pcb[i+N].t_name);
                    printf("请输入进程运行时间:");
                    scanf("%f",&pcb[i+N].t_time);
                    getchar();
                    printf("请输入进程优先级:");
                    scanf("%d",&pcb[i+N].priority);
                    N++;///当前进程数加1
                }else{
                    printf("请输入进程id:");
                    scanf("%ld",&id);
                    ///当就绪队列不为空时,需要检查所创建的进程id是否重复
                    for(j=1;j<i+N;){
                        if(id==pcb[j].t_id){
                            printf("进程ID已经存在!\n请重新输入:");
                            scanf("%ld",&id);
                            j=0;
                        }else{
                            j++;
                        }
                    }
                    pcb[i+N].t_id=id;
                    getchar();
                    printf("请输入进程名:");
                    gets(pcb[i+N].t_name);
                    printf("请输入进程运行时间:");
                    scanf("%f",&pcb[i+N].t_time);
                    getchar();
                    printf("请输入进程优先级:");
                    scanf("%d",&pcb[i+N].priority);
                    N++;///当前进程数加1
                }
                ///将当前进程加入就绪队列
                t=malloc(sizeof(T));
                t->data=N;
                t->next=NULL;
                ///将就绪队列中的进程加入消息缓冲队列
                L.rear->next=t;
                L.rear=t;
    
            }
        Sort();
        return 0;
    
    }
    void Sort(){
        int i,j;
         ///将进程按优先级排序
        for(i=1;i<=N;i++){
            for(j=i+1;j<=N;j++){
                if(pcb[i].priority<pcb[j].priority){
                    PCB temp=pcb[i];
                    pcb[i]=pcb[j];
                    pcb[j]=temp;
                }
            }
    
        }
    }
    
    ///显示已经就绪的进程
    void display(){
        T p;
        p=L.front;
        if(p->next==NULL){
            printf("当前无进程!");
        }
        while(p->next!=NULL){
            p=p->next;
            printf(" 进程ID:%ld\n 进程名:%s \n运行时间:%f \n优先级:%d\n",pcb[p->data].t_id,pcb[p->data].t_name,pcb[p->data].t_time,pcb[p->data].priority);
        }
    }
    ///进程终止
    void kill(){
        int id;
        T p;
        p=L.front;
        if(p->next==NULL){
            printf("当前无进程!\n\n");
        }else{
            printf("输入要终止的进程的id:");
            scanf("%ld",&id);
            while(pcb[p->next->data].t_id!=id){
                if(p->next==NULL){
                    printf("该进程不存在!\n");
                    return 0;
                }
                p=p->next;
            }
            if(pcb[p->next->data].t_id==id){
                p->next=p->next->next;
                
                /*当L.front->next==NULL时,
                说明当前杀死的进程是系统中最后一个进程,
                此时首位指针指向同一个位置*/
                
                if(L.front->next==NULL){
                    L.front=L.rear;
                }
                /*当p->next==NULL时,
                说明当前杀死的进程是队尾进程,
                此时尾指针指向p*/
                if(p->next==NULL){
                    L.rear=p;
                }
                ///删除已终止进程的PCB进程控制块信息
                while(p->next!=NULL){
                    p->next->data--;
                    pcb[p->next->data]=pcb[p->next->data+1];
                    p=p->next;
                }
            }
        printf("成功杀死进程!\n");
        return 0;
        }
    
    }
    void menu(){
        int n;
        while(1){
            printf("进程演示系统\n");
            printf("**************************************************\n");
            printf("1.创建进程\n");
            printf("2.查看进程\n");
            printf("3.杀死进程\n");
            printf("4.退出程序\n");
            printf("请输入你的选择(1-4):");
            scanf("%d",&n);
            switch(n){
                case 1:Create();
                break;
                case 2:display();
                break;
                case 3:kill();
                break;
                case 4:return 0;
                default:printf("没有这个选项!");
                break;
            }
        }
        return 0;
    }
    int main()
    {
        InitQueue();
        menu();
        return 0;
    }
    
    展开全文
  • 进程创建(fork原理)

    千次阅读 2019-06-01 14:18:40
    调用一次,两个返回值,是由于系统调用fork函数会创建进程,(如果创建失败返回-1)如果创建成功则,子进程返回值为0,父进程的返回值是子进程的pid(进程标识符);这样就可以通过父子进程的返回值来判断到底是子...
  • 封装进程创建的原因主要是易语言自带的运行和执行命令不能启动游戏,当我们写一键启动,批量启动游戏时,便用到了进程_创建的命令。易语言里的运行(),执行()有时候并不能直接运行一些游戏,而采用进程创建才...
  • 会引起进程创建的事件

    千次阅读 2021-02-24 16:04:13
    因此为了使程序运行必须为其创建进程,而导致进程创建的时间典型的有四种: 用户登录;系统为用户创建一个进程,并插入就绪队列 作业调度 提供服务 系统为用户请求创建一个进程 应用请求 用户程序自己创建进程 .....
  • 操作系统之进程创建

    万次阅读 2019-06-05 21:31:16
    操作系统基于某种原因决定创建一个新进程时,会按如下步骤操作: 为新进程分配一个唯一的进程标识符。此时,主进程表中会添加一个新表项,每个进程一个表项。 为进程分配空间。这包括进程映像中的所有元素。因此,...
  • c语言进程创建、等待和退出

    千次阅读 2019-04-16 23:06:48
    c语言进程创建、等待和退出 创建进程 fork()函数:函数返回值类型为pid_t 若程序运行在父进程中,函数返回的PID为子进程今年称号; 弱运行在子进程中返回PID为0. #include <sys/types.h> #include <...
  • (1)理解进程创建相关理论。 (2)掌握进程创建方法。 (3)掌握进程相关数据结构。 【课题描述】 本课题针对操作系统中进程的创建相关理论进行设计。要求编写程序并进行测试。代码简化了进程创建的多个步骤和内容...
  • 进程创建 1、fork函数 fork函数很重要,函数功能是:从已存在的进程中创建一个新进程,新创建出的进程为子进程,而原进程为父进程。 #include&amp;amp;lt;unistd.h&amp;amp;gt; pid_t fork(void);...
  • LInux进程创建过程

    千次阅读 2019-04-15 22:58:41
    传统的fork系统调用直接把所有的资源复制给新创建进程,但是这种实现过于简单,效率低下,因为并不支持拷贝数据的共享。 更糟的是如果新进程打算立即执行一个新的映像那么所有的拷贝都将前功尽弃。 Linux下面的...
  • /*进入子进程创建子进程*/ #include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #include &lt;unistd.h&gt; #include &lt;signal.h&gt; #include &lt;sys/wait.h&gt; int...
  • linux C 进程 创建进程 详解

    千次阅读 2017-11-25 21:46:55
    首先介绍一下进程创建方法,fork与vfork函数 fork函数  一个进程调用fork()函数后,系统先给新的进程分配资源,包括代码、数据和分配给进程的资源,然后把原来的进程的所有值都复制到新的新进程中,只有...
  • 一个父进程创建多个子进程

    千次阅读 2018-08-07 20:59:49
    循环方式 #include #include #include #include ...#define child 10 ...int main(int argc, char const *argv[]) ... //如果在此位置,就是子进程创建... //如果在此位置,就是一个父进程创建多个子进程 } }  
  • 一、父进程创建子进程,使用fork(), 一次fork() 两次返回,一次返回的是父进程自己,一次返回的是子进程的运行空间。 二、父进程必须监控子进程的运行状态,等待子进程退出后,使用wait()进行关闭,否则,如果父...
  • 今天介绍进程的进程的管理,子进程创建以及进程资源的回收 首先什么是进程? 答:进程是程序的实例。程序是静态的,是存放在硬盘上的,程序运行起来就形成了进程。程序从磁盘到内存里之后就形成了进程。 进程又分为...
  • 在Linux下我们可以用fork函数创建一个子进程,但是当我们需要在一个父进程创建多个子进程时,有些需要注意的地方。假设我们用如下代码在一个父进程创建两个子进程: void main() { pid_t pid1, pid2; pid1 = ...
  • PsSetCreateProcessNotifyRoutine函数用来注册一个进程创建的回调函数,当有新从进程被创建时,就把父进程的ID,和子进程(被创建的进程)ID传给回调函数,通过回调函数,可以监控新创建的进程 NTSTATUS ...
  • 引起进程创建的事件有哪些?

    万次阅读 2017-06-09 21:03:19
    进程控制的功能有:进程创建,进程阻塞,进程唤醒,进程撤销。进程创建是进进程控制的基本功能之一。引起进程创建的事件。 1,系统生成时,系统会创建承担资源分配和管理的系统进程。 2,用户作业调入系统时,作业...
  • 进程创建子进程后,父进程与子进程同时执行(并发)。 主程序调用子程序后,主程序暂停在调用点,子程序开始执行,直到子程序返回,主程序开始执行。 我们可以这么理解:父进程运行的程序,创建了子进程,此时父...
  • Linux进程创建及同步实验

    千次阅读 2018-07-14 21:38:40
    ①编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程输出不同的内容。试观察记录屏幕上的显示结果,并分析原因。②修改上述程序,每一个进程...
  • 进程创建&父子进程资源分配

    千次阅读 2016-08-04 16:20:03
    父子进程创建完子进程后互相不关联,以独立身份抢占 CPU 资源,具体谁先执行由调度算法决定,用户空间没有办法干预。子进程执行代码的位置是 fork/vfork 函数返回的位置。 2.子进程资源申请问题。 子进程重新...
  • 进程和渲染进程的区别 主窗体html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"&...
  • Windows 驱动阻止进程创建

    千次阅读 2016-10-19 22:18:47
    windows Vista及以后版本可以使用PsSetCreateProcessNotifyRoutineEx函数检测到进程创建和退出,当进程创建时什么回调函数中的参数的成员CreateStatus为不成功的值即可阻止进程创建,但是一般会弹出一个对话框,这个...
  • 编写一段程序,使用系统调用fork()创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符:父进程显示“a“;子进程分别显示字符”b“和字符“c”。试观察记录...
  • 操作系统实验——进程创建与并发

    千次阅读 2017-02-15 16:24:57
    * func: 利用fork()创建进程 * 利用kill()杀死进程 * **/ #include<sys/types.h> #include<stdio.h> #include<unistd.h> #include<signal.h> #include<wait.h> int main() { pid_...
  • Win10 Hook 进程创建的研究

    千次阅读 2016-11-21 19:40:03
    Win7甚至在它之前开始,想要hook进程创建已经不能简单的HookCreateProcessInternalW这一个函数了,因为Vista开始引入了UAC机制。如果一个进程想要通过UAC弹窗创建管理员进程,hook这个进程的CreateProcessInternalW...
  • 通过进程创建、撤销和运行加深对进程概念和进程并发执行的理解,明确进程与程序之间的区别。通过本实验提高分析问题和解决问题的能力,并学习撰写规范的科学研究报告。 实验内容: (1)了解系统调用fork(), exec...
  • OS第二篇:进程创建fork()函数实验

    千次阅读 2019-04-29 19:06:47
    编写一段程序,利用系统调用fork()函数创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符串,其中,每个进程显示其pid值,及其父进程的pid值。(getpid返回...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,411,433
精华内容 564,573
关键字:

进程创建