• 读书笔记


    第三章 进程管理

    1.摘要

    • 本章讨论了 Unix/Linux 中的进程管理;阐述了多任务处理原则;介绍了进程概念;以一个编程示例来说明多任务处理、上下文切换和进程处理的各种原则和方法。多任务处系统支持动态进程创建、进程终止,以及通过休眠与唤醒实现进程同步、进程关系,以及二叉树的形式实现进程家族树,从而允许父进程等待子进程终止;提供了一个具体示例来阐释进程管理函数在操作系统内核中是如何工作的;解释了 Unix/Linux 中各进程的来源,包括系统启动期间的初始进程、INIT 进程、守护进程、登录进程以及可供用户执行命令的 sh 进程;对进程的执行模式进行了讲解,以及如何通过中断、异常和系统调从用户模式转换到内核模式;描述了用于进程管理的 Unix/Linux 系统调用,包括 fork、wait、exec 和 exit ;阐明了父进程与子进程之间的关系,包括进程终止和父进程等待操作之间关系的详细描述;解释了如何通过 INIT 进程处理孤儿进程,包括当前 Linux 中的 subreaper 进程,并通过示例演示了 subreaper 进程;详细介绍了如何通过 exec 更改进程执行映像,包括 execve 系统调用、命令行参数和环境变量;解释了 I/0 重定向和管道的原则及方法,并通过示例展示了管道编程的方法。

    2.基本概念

    2.1 多任务处理
    32位windows操作系统中,多任务处理是指系统可同时运行多个进程,而每个进程也可同时执行多个线程。一个线程是指程序的一条执行路径,它在系统指定的时间片中完成特定的功能。系统不停地在多个线程之间切换,由于时间很短,看上去多个线程在同时运行。或者对于在线程序可并行执行同时服务于多个用户称为多任务处理。
    引入多任务处理是为了提高CPU的利用率。Windows、IOS、Android等操作系统都支持多任务处理。Windows 中的前台与后台任务都能分配到CPU的使用权。

    2.2 进程的概念

    • 定义:
      - 狭义定义:进程是正在运行的程序的实例。
      - 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
      进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

    • 特征

      • 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
      • 并发性:任何进程都可以同其他进程一起并发执行
      • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
      • 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
      • 结构特征:进程由程序、数据和进程控制块三部分组成。
      • 多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
    • 进程的分类
      - 交互进程:有输入输出,用户可以根据自己的情况输入数据,得到想要的结果(一般情况下的进程)
      - 批处理进程:由脚本加载执行的程序(Linux下的shell,Windows下的bat)
      - 守护进程:总是活跃的,后台执行,开机时加载执行或root用户手动加载执行

    • 查看进程
      – ps:显示当前用户有终端控制权的进程信息
      – ps aux:以列表形式显示详细信息
      – a:所有用户的终端控制进程
      – x:所有用户的无终端控制进程
      – u:详细的显示方式

    • 进程的详细信息列表

    – USER:进程的属主
    – PID:进程号(ID)
    – %CPU:CPU使用率
    – %MEM:内存使用率
    – VSZ :占用虚拟内存大小
    – RSS:占用物理内存大小
    – TTY:有终端控制的显示终端的次设备号,无终端控制的显示”?“
    – STAT:进程的状态
    — O:就绪态-等待被系统调度
    — R:运行态-Linux系统下没有O,就绪态也用R表示
    — S:休眠态-可以被系统中断,信号唤醒
    — T:暂停态-是被SIGSTOP信号暂停的,当收到SIGCONT信号时才能再转入运行态
    — Z:僵尸态-已经结束停止运行,但父进程还没有回收
    — <:高优先级进程
    — N:低优先级进程
    — l:多线程化的进程
    — +:在前台进程组中的进程
    — s:会话首进程
    – START TIME:进程的开始时间
    – COMMAND:进程的可执行文件名
    

    2.3 进程标识符
    操作系统会为每个进程分配一个唯一的标识符,采用无符号整数表示,即进程ID。
    进程ID在任何时候都是唯一的,但是可以重用,当一进程结束,新创建的进程才可以使用它的进程ID(延时重用)。
    pid_t getpid(void);功能:获取进程ID
    pid_t getppid(void);功能:获取父进程ID
    uid_t getuid(void);功能:获取当前进程的用户ID
    gid_t getgid(void);功能:获取当前进程的组ID
    pid_t getpgid(pid_t pid);功能:获取pid进程的进程组ID
    int setpgid(pid_t pid, pid_t pgid)功能:设置进程pid的进程组ID


    3.进程创建

    3.1 fork

    #include <unistd.h>
    pid_t fork(void);
    
    • 功能:创建一个新进程
    • 返回值:一次调用两次返回,失败返回-1(当进程数走出系统的限制进程创建就会失败)
    • 两次返回分别是进程ID和0,父进程拿到子进程ID,子进程返回0,借此可以分出父子进程,编写不同的处理分支。一般解决一些多任务,并发执行
    • 通过fork创建的子进程就是父进程的副本(拷贝),子进程会获取父进程的数据段,bss段,堆,栈,IO流(共享文件指针和文件描述符),缓冲区的拷贝,与父进程共享代码段。
    • 子进程会继承父进程的信号处理方式。
    • fork函数调用后,父子进程各自执行,谁先返回不一定,但是可以使用一些手法来规定谁先执行。
      由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。而是使用了写时复制技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一”页”。

    fork有下面两种用法:
    1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。(开始时只有一个进程,后来fork出了两个)
    2、一个进程要执行一个不同的程序。在这种情况下,子进程从fork返回后立即调用exec(创建了一个全新进程)子进程在fork和exec之间可以更改自己的属性。例如I/O重定向,用户ID、信号安排等。

    //fork函数示例
    //fork就是分支的起点
    //之前是一个进程,遇到fork之后便一分为二,成两个进程。
    #include <unistd.h>
    #include <stdio.h>
    #include <errno.h>
    #include <stdlib.h>
    
    int  glob = 6 ;
    char buf[] = "a write to stdout
    " ;
    
    int 
    main(int argc, char** argv)
    {
        int   var ;
        pid_t pid ;
    
        var = 123;
        if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
            perror("write error") ;
    
         printf("before fork
    ") ;
    
         if ((pid = fork()) < 0)
            perror("fork error") ;
        else if (pid == 0) //子进程
        {
            glob++ ;
            var++ ;
        }
        else //父进程
        {
            sleep(3) ; //挂起3秒,让子进程先运行
        }
    
        //父子进程都有的 相同的程序正文
        printf("pid = %d, glob = %d, var = %d
    ", getpid(), glob, var) ;
       exit(0) ;
    }
    

    在fork进程时,注意标准I/O的缓冲问题
    write函数不带缓冲的,但标准I/O库是带缓冲的。如果标准输出连到终端设备,则它是行缓冲的(由换行符冲洗),否则它是全缓冲的
    若把上面程序的输出重定向到文件:./a.out > test.txt 则"before fork "会输出两次。原因是当将标准输出重定向到一个文件时,标准I/O是全缓冲的。在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程中。于是那时父、子进程各自有了带该行内容的标准I/O缓冲区。当每个进程终止时,最终会冲洗其缓冲区中的副本。


    4.进程的退出

    4.1 五种正常退出
    1.从main函数中return
    2.调用标准库中的exit函数
    void exit(int status);
    功能:调用者立即结束该进程
    status:退出状态码,可以再父进程中获取到,子进程留给父进程的遗言
    退出前做的事情:

    • 1.先调用实现注册的函数(atexit/on_exit)
      • int on_exit(void (*function)(int , void *), void *arg);

    功能:注册一个函数,当进程通过exit函数开始结束时调用
    function:函数指针,无返回值,参数1为exit函数的参数,参数二,为on_exit函数的第二个参数.
    arg:当function函数被调用是传递给它第二参数。

      • int atexit(void (*function)(void));

    功能:注册一个函数,当进程通过exit函数结束时调用。
    function:函数指针,无返回值无参数。

    • 2.fflush:冲刷所有处在未关闭状态的标准IO流,删除所有临时文件
    • 3.返回一整数给操作系统,一般为0(EXIT_SUCCESS)或1(EXIT_FAILURE)
    • 4.该函数不会返回并且它的功能借助了_exit /_Exit

    3.调用_exit/_Exit函数退出
    注意:这两个函数的功能是一样的

    #include <unistd.h>
    void _exit(int status);
    
    #include <stdlib.h>
    void _Exit(int status);//调用系统的_exit
    
    • 功能
      • 调用的进程会结束,没有返回值
    • status
      • 会被父进程获取到(低八位,一个字节)
      • 进程结束前会关闭所有处于打开状态的文件描述符。
      • 把所有的子进程托付给init(孤儿院)
      • 向它的父进程发送SIGGHLD信号。

    注意:exit也会进行以上操作。(底层调用了这两个函数)
    4.进程的最后一个线程执行了结束语句
    5.进程的最后一个线程调用了pthread_exit函数

    4.2 三种异常退出
    1.调用了abort函数,该函数会产生SIGABRT信号
    2.进程接收到一些信号(无法捕获处理、或无法捕获处理)
    3.进程的最后一个线程收到取消请求,并做出相应,相当于线程收到了结束信号。

    4.4 僵尸进程
    僵死进程:一个已经终止,但是其父进程尚未对其进行善后处理(获取终止子进程的终止状态信息,释放它占用的资源)的进程被称为僵死进程(zombie)。[即:已死,但无人收尸]
    由init领养的进程不会变成僵死进程。因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这也就防止了系统中有很多僵死进程。
    (这只能做到父进程先死,子进程不会变僵死进程。若子进程先死,则防止僵死进程的责任就交给我们了。---内核在父进程终止时只检查其活着的子进程。)

    4.3 防止僵死进程
    若在父进程中调用waitpid函数,则它只能获取第一个终止的子进程状态,其他子进程可能变为僵死进程。若在调用waitpid之前就有子进程结束,则更糟。
    若在SIGCHLD的信号处理函数中调用waitpid,则效果好一些,但也可能会产生僵死进程。因为若在信号处理函数执行期间,又有多个子进程结束,发出SIGCHLD信号,UNIX系统只投递一次信号。这样会有子进程的终止状态得不到获取。

    • 有效方式1:父进程调用sigaction函数绑定信号SIGCHLD的信号处理函数时,把其选项字段设置为SA_NOCLDWAIT,则可防止僵死子进程。(子进程终止后, 内核自动把其终止状态信息丢弃)父进程可随时结束,不必等到所有子进程终止。 详情见UNIX 信号博文
    • 有效方式2:调用fork两次以避免僵死进程。
    //调用fork两次,以避免僵死进程。
    #include <unistd.h>
    #include <stdio.h>
    #include <errno.h>
    #include <stdlib.h>
     
    int
    main(void)
    {
        pid_t pid ;
     
        if ((pid = fork()) < 0)
            perror("fork error") ;
        else if (pid == 0)  //子进程的作用就是创建孙进程,然后把它托付给init进程
        {
            if ((pid = fork()) < 0)
                perror("fork error") ;
            else if (pid == 0) //以下就是实际做事的 孙进程1代码段
            {
                sleep(2) ; //要让子进程先运行完 终止
                //打印出其父进程ID
                printf("grandchild 1, parent pid = %d
    ", getppid()) ;
                exit(0) ;
            }
     
            if ((pid = fork()) < 0)
                perror("fork error") ;
            else if (pid == 0) //以下就是实际做事的 孙进程2代码段
            {
                sleep(2) ;
                //打印出其父进程ID
                printf("grandchild 2, parent pid = %d
    ", getppid()) ;
                exit(0) ;
            }
     
            //终止自己,这样init就领养了各孙进程
            exit(0) ;
        }
       
        //以下是父进程代码段
        //父进程需要等待子进程(防止子进程变zombie)但这种等待时间极短(子进程很快便终止了)
        if (waitpid(pid, NULL, 0) != pid)
            perror("waitpid error") ;
     
        exit(0) ;
    }
    


    5.wait / waitpid

    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
    
    • 功能:等待所有子进程结束,并获取到最终的状态码,只要有一个进程结束就立即返回。
    • 父进程收到子进程发送来的SIGCHLD信号时,调用wait函数回收子进程的资源并获取结束状态。
    • 如果所有子进程都在运行,则wait阻塞。
    • 如果已有僵尸进程,wait也会立即返回,回收资源获取结束状态码。
    • 如果没有子进程,则返回失败-1。
      pid_t waitpid(pid_t pid, int *status, int options);
    • 功能:等待指定的进程结束,并获取到最终的状态码。
    • pid:
      • 负1:等待任一子进程结束,此时与wait等价
      • 大于0:等待进程号为pid的进程结束,此时只等待一个进程结束
      • 等于0 :等待同组的任一子进程结束, 此时等待的是整个进程组
      • 小于负1:等待的是进程组id是pid的绝对值中的任一子进程结束,此时等待的是整个进程组。
    • options:
      • WNOHANG 非阻塞模式,如果没有子进程结束则立即退出
      • WUNTRACED 如果子进程处理暂停,则返回它的状态
      • WCONTINUED 如果子进程从暂停转为继续,则返回它的状态

    注意:

    • 1.wait函数只能孤独的等待子进程结束,而waitpid可以有更多的选择。
    • 2.waitpid不光可以等待子进程,也可以等待同组进程。
    • 3.waitpid可以阻塞也可以不阻塞。
    • 4.也可以监控子进程的暂停或继续状态。

    6.执行程序

    6.1exec
    加载子进程的可执行文件。

    #include <unistd.h>
    

    int execl(const char *path, const char *arg, ...);

    • path:可执行文件的路径
    • arg:第一个main函数的参数,最后一次必须以NULL结尾。
    • int execlp(const char *file, const char *arg,...);
    • file:可执行文件的名字,会从PATH环境变量的路径中查找可执行文件并执行。
    • arg:第一个main函数的参数,最后一次必须以NULL结尾。

    int execle(const char *path, const char *arg,..., char * const envp[]);

    • path:可执行文件的路径
    • arg:第一个main函数的参数,最后一次必须以NULL结尾。
    • envp:父进程的环境变量表,传递给子进程。

    int execv(const char *path, char *const argv[]);

    • path:可执行文件的路径
    • argv:命令行参数

    int execvp(const char *file, char *const argv[]);

    • file:可执行文件的名字,会从PATH环境变量的路径中查找可执行文件并执行。
    • argv:命令行参数

    int execvpe(const char *file, char *const argv[],char *const envp[]);

    • file:可执行文件的名字,会从PATH环境变量的路径中查找可执行文件并执行。
    • argv:命令行参数
    • envp:父进程的环境变量表,传递给子进程。

    6.2system

    #include <stdlib.h>
    int system(const char *command);
    
    • 功能

    执行系统命令的,也可以加载可执行程序。
    相当于创建一个子进程,但子进程不结束,该函数不返回,父子进程不会同时执行。
    该函数的实现应该调用了:fork、exec等函数。


    7.I/O重定向

    I/O重定向是一个过程,这个过程捕捉一个文件、命令、程序或脚本,甚至代码块的输出,然后把捕捉到的输出作为输入发送给另外一个文件、命令、程序或脚本。

    7.1文件描述符
    文件描述符是从0开始到9的结束的整数,指明了与进程相关的特定数据流的源。当Linux系统启动一个进程(该进程可能用于执行shell命令)时,将自动为该进程打开三个文件:标准输入(文件标识符为0)、标准输出(1标识)和标准错误输出(2标识),若要打开其他的输入或输出文件则从整数3开始标识。默认情况下,标准输入与键盘输入相关联,标准输出与标准错误输出与显示器相关联。

    Shell从标准输入读取输入数据,将输出送到标准输出,如果该命令在执行过程中发生错误,则将错误信息输出到标准错误输出。

    tee命令将shell的输出从标准输出复制一份到文件中,tee命令加-a表示追加到文件的末尾。

    7.2I/O重定向符号
    I/O重定向符号分为:基本I/O重定向符号和高级I/O重定向符号(与exec命令有关)。

    符号是强制覆盖文件的符号,如果noclobber选项开启(set -o noclobber),表示不允许覆盖任何文件,此时>|可强制将文件覆盖。n>> file、n>|file与n>file都是将FD为n的文件重定向到file文件中。
    - <是I/O重定向的输入符号,它可将文件内容写到标准输入之中。wc -l < newfile,其中shell从命令行会“吞掉”<newfile并启动wc命令。
    <<delimiter(delimiter为分界符),该符号表明:shell将分界符delimiter之前的所有内容作为输入,cat > file << FIN,输入FIN后按回车键结束编辑,输入内容重定向到file文件中。其另一种形式:-<<delimiter,在<<前加一个负号,这样输入文本行所有开头的"Tab"键都会被删除,但开头的空格键却不会被删除,如cat > file -<< FIN。

  • 相关阅读:
    三种按键处理函数
    enum与typedef enum的用法
    PIC18F中断定时器
    .net core Ocelot+Consul实现网关及服务注册和服务发现
    wpf的优点
    ASP.NET Core Web API下事件驱动型架构的实现
    2020个人计划
    图解C#的值类型,引用类型,栈,堆,ref,out
    .NET Core中的一个接口多种实现的依赖注入与动态选择
    redis分布式锁深度剖析
  • 原文地址:https://www.cnblogs.com/tzy20191327/p/15443080.html
Copyright © 2020-2023  润新知