• 进程控制


    进程控制介绍

      进程控制中涉及到进程创建、睡眠、退出等,在Linux中提供fork、clone的进程创建方法,sleep的进程睡眠,exit的进程终止调用。

    主要的系统调用

      

       下面将具体介绍重要的系统调用的代码实现。

    fork创建进程

      我们可输入man 2 fork查看该函数的声明

      

       由图可知函数声明在头文件<unistd.h>中,且fork的类型为pid_t,这个类型与进程标识符PID的类型是一样的,所以我们这样写代码也是OK的:

    pid_t pid;
    pid = fork();  

      fork的中文名为分叉,十分形象。在每次执行fork后,就会产生一个新的进程,这样就分叉了,当前进程称为子进程,原来的进程称为父进程。

      fork的特点在于一次性返回两个值。先来分析下面的代码:

    int main()
    {
      int i;
      if (fork() == 0)
      {
        for (i = 1; i < 3; i++)
        printf("This is child process
    ");
      }
      else
      {
        for (i = 1; i < 3; i++)
        printf("This is parent process
    ");
      }
    }

      执行结果如下:

    This is child process
    This is child process
    This is parent process
    This is parent process
    exe

      if和else里头的代码都执行了,说明fork()的返回值必然一个为0另一个为非0。

      通过查阅资料知道,在执行fork()时,由于fork创建了一个子进程,所以fork()会将子进程的PID(进程标识符为一个正值)返回给父进程,而将0返回给刚创建的子进程。如果想得到父进程的PID,用函数getppid(),如果想得到子进程的PID,则用函数getpid(),这两个函数的返回值类型都是int。

      额外补充一点,fork()创建失败的话,返回值为-1,失败的具体情况有下两点:

      1. 父进程拥有的子进程个数超过了限制。
      2. 提供使用的内存不足

      另外我们把for循环的次数放大,比如每个for循环执行5000次,会发现一个有趣的现象,即开始交错打印:

      

      则明显地看到父进程与子进程的并发执行。一般来说是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。但是由于操作系统一般让所有进程都享有相同的执行权,除非另一个优先权更高。所以这里的父进程与子进程的执行会交替进行。

      如何避免父进程与子进程的并发执行呢?可以使用vfork()函数,它的语法与fork()一致。vfork的特性之一是保证子进程先执行,等到它调用exit();或者exec的时候,父进程才可以调度执行。

      vfork的另一个特性是用其创建的子进程共享父进程的地址空间,也就是说子进程完全运行在父进程的地址空间之上。若子进程与父进程有相同的局部变量、全局变量,那么子进程修改任一变量的值,父进程的对应变量的值也被修改。而fork则不同,子进程与父进程是独立的,这就是说子进程里修改任一变量的值,父进程是不受影响的。而且正因为fork创建的子进程的地址空间的与父进程的地址空间是彼此独立,所以父进程才要将几乎是一切的资源都拷入子进程中,这是非常浪费的行为。

      再说一个fork的特性,就是子进程会继承父进程的数据段、堆栈段,还有缓冲区。这意味着如果你不刷新缓冲区,之前在父进程缓冲区中的数据还会被继承到子进程中。刷新缓冲区的办法有:1、printf中的   2.fflush(stdout); 。

      举例:

    //例一
    printf("hello ");
    fork();
    printf("world  ");  //最后输出hello world  hello world
    
    //例二
    printf("hello ");
    fflush(stdout);
    fork();
    printf("world  ");  //最后输出hello world world
    
    //例三
    printf("hello
    ");
    fork();
    printf("world  ");  //最后输出hello world world

    exit进程退出

      Linux系统中进程退出的方式分为正常退出和异常退出两种,其中正常退出的方法有三种,异常退出的方法有两种。

      正常退出: 1.main函数中的return 2.调用exit 3.调用_exit

      异常退出:1.调用about 2.进程接收到某个信号而终止

      return与exit的区别是:exit是有参数,return是执行完函数后的返回;exit结束后将控制权交给系统,return结束后将控制权交给调用函数

      exit与_exit的区别是:exit在头文件stdlib.h声明,_exit在头文件unistd.h声明。结束进程后,_exit会立即返回给内核,而exit要清除一段指令后才返回给内核。

    exec进程执行新程序

       一般在子进程中会使用exec函数来执行另一个程序。系统调用exec用于执行一个可执行程序以代替当前的程序,也就是说某进程一旦调用了 exec 类函数,正在执行的程序就被干掉了,系统把代码段替换成新的程序(由 exec 类函数执行)的代码,并且原有的数据段和堆栈段也被废弃,新的数据段与堆栈段被分配,但是进程号却被保留。

      所以exec的执行结果为:系统认为该进程还是原来的进程,但是进程里面的程序被替换了。

      下面是通过man 3 exec得到的exec函数族的相关信息

      

       将上面的库函数声明归纳后,如下图

      

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

      这儿出现了一个陌生的外部全局变量 char **environ,这是什么呢? 它是环境变量。

      所谓环境变量,就是为了便于用户灵活地使用shell,Linux引入了环境变量的概念,包括了用户的主目录、当前目录、终端类型等,它们定义了用户的工作环境,所以称为环境变量。通过命令env可查看这些环境变量值,或者通过以下方式也能实现查看环境变量值。

    extern char **environ;
    for(i=0; environ[i] != NULL; i++)
        printf("%s",environ[i]);

      另外,事实上main函数的完整形式是:int main(int argc, char *argv[], char **envp)。

      关于exec函数族的调用举例:

      

      事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,这些函数之间的关系如下图所示:

      

      实例:(实现IO重定向)把标准输入转成大写然后打印到标准输出

      分析:调用exec后,原来打开的文件描述符仍然是打开的。利用这一点可以实现I/O重定向。

    /* upper.c */
    #include <stdio.h>
    
    int main(void)
    {
        int ch;
        while((ch = getchar()) != EOF) {
        putchar(toupper(ch));
        }
        return 0;
    }
    upper.c
    /* wrapper.c */
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <fcntl.h>
    int main(int argc, char *argv[])
    {
        int fd;
        if (argc != 2) {
        fputs("usage: wrapper file
    ", stderr);
        exit(1);
        }
    
        fd = open(argv[1], O_RDONLY);
        if(fd<0) {
        perror("open");
        exit(1);
        }
    
        dup2(fd, STDIN_FILENO);
        close(fd);
    
        execl("./upper", "upper", NULL);
        perror("exec ./upper");
        exit(1);
    }
    wrapper.c

        wrapper程序将命令行参数当作文件名打开,将标准输入重定向到这个文件,然后调用exec执行upper程序,这时原来打开的文件描述符仍然是打开的,upper程序只负责从标准输入读入字符转成大写,并不关心标准输入对应的是文件还是终端。

      

     wait等待进程结束

      当子进程先于父进程退出时,若无调用wait()或waitpid(),子进程就会进入僵尸进程。若调用,则不会变为僵尸进程。

      系统调用声明如下:

      

      wait():系统让父进程暂停执行直到它的一个子进程停止。返回值是该子进程的PID。

      waitpid():与wait同理,不过它是让特定的的子进程停止且需要提供PID。参数pid指明了要等待的子进程的PID。pid值的意义见下表。options 参数允许用户可改变waitpid()的行为,若将该参数赋值为WNOHANG,则父进程不被挂起而立即返回并执行其后的代码。

      

      它们都有int *status这个参数,这个参数指向的变量存放了子进程的(状态信息),比如子进程main()返回的值或者子进程中exit()的参数。

      让父进程周期性检查特定的子进程是否已经退出:

    waitpid(child_pid, (int*)0, WNOHANG );

      如果子进程未退出,则返回0,若子进程已退出,则返回child_pid。调用失败则返回-1。失败的原因可能是子进程不存在、参数不合法等。

      注意:waitpid是wait的非阻塞版本,如果希望父进程在查看子进程状态的同时不被阻塞,可使用waitpid与WNOHANG选项。

      

      

      

    未完待续  

    ————全心全意投入,拒绝画地为牢
  • 相关阅读:
    storage存储对象和数组类型时候的问题
    关于vue-router路径配置的问题
    解决v-for产生的警告的办法
    网页调用打印机打印文件
    vue-router的link样式设置问题
    在vue项目当中使用sass
    使用正则获取地址栏参数的方法
    escape、encodeURI和encodeURIComponent的区别
    SQLServer安装错误之--->无法打开项 UNKNOWNComponentsDA42BC89BF25F5BD0AF18C3B9B1A1EE8c1c4f01781cc94c4c8fb1542c0981a2a
    软件收集网站!
  • 原文地址:https://www.cnblogs.com/Bw98blogs/p/7604493.html
Copyright © 2020-2023  润新知