美文网首页
Linux高级环境编程之7执行单元_进程管理

Linux高级环境编程之7执行单元_进程管理

作者: 编程半岛 | 来源:发表于2018-08-13 14:12 被阅读12次

    课程目标

    (1) 掌握进程的基本概念,进程属性获取。
    (2) 掌握进程的生命周期以及资源申请与释放的过程。
    (3) 掌握创建新进程时父子进程资源的管理。
    (4) 掌握守候进程等特殊进程的管理。
    (5)多进程在编程中的应用。

    主要知识点

    (1) 程序,进程,进程资源。
    (2) 进程的生命周期,进程状态。
    (3) 进程创建与父子进程资源。
    (4)进程属性管理与进程应用。

    1. 程序、进程、进程属性与进程状态

    • 进程是unix/Linux中基本的资源管理单元。
    • 进程又是执行的代码片段(一个进程可能创建多个进程,一个进程可以执行多个程序的代码)。将一个代码片段加载到内存并让其执行也就是创建一个(或多个)进程。
    • 进程与程序的关系:程序存储在磁盘上,是一个文件;而进程是一个加载到内存执行的程序段,且有生命周期,创建、执行、退出、等待的状态
    • 一个进程不仅仅占用了加载代码的内存(用户空间),在Linux下,使用task_struct这个结构体来维护整个进程的资源(内核空间)
    • 在内核中task_struct完整的描述了一个进程的所有信息:
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */ 描述了状态信息
    struct mm_struct *mm, *active_mm; 与内存存储相关信息,描述了在用户空间中的代码段,数据段,堆, 栈, mmap等涉及的所有内存信息
    /* open file information */
    struct files_struct *files;  打开文件的信息列表,成员fd_array[NR_OPEN_DEFAULT]就是描述了这个进程打开的文件列表。
    /* signal handlers */
    struct signal_struct *signal;  信号的描述信息
    struct sighand_struct *sighand; 信号的描述信息
    
    • 应用编程时产看进程ps aux
    • 用户空间的进程属性:
      PID:进程号,每个进程有唯一的编号
      状态:如下描述

    Linux系统是一个多任务多用户的系统。为什么会用多进程,意义何在?
    现在CPU的速度非常快,而人的反映时间时微秒级,系统的外设的速度的速度也相对比较慢。如果使用单进程任务系统,则CPU有大量空闲。因此,我们让CPU在各个执行单元中不停的调转。当某个进程需要等待其他的资源时,CPU转而执行其它的进程,充分利用了CPU资源。在什么时候执行哪一个进程实质是由调度算法来决定的。
    CPU只有一个,而进程有多个,因此某个时刻只能执行一个进程,其他的进程则处于相应的其他状态
    运行状态:占有资源,执行;
    就绪状态:除了CPU资源外,其他资源都已获取,等待调度算法来执行;
    等待状态:除了CPU资源外,还需要等待其他资源或时间,分为可中断等待(可以被信号打断)和不可中断等待
    停止状态:正在被跟踪或者调试的进程
    僵死状态:用户资源已经收回,PCB内存资源没有收回,已经不能执行

    怎么来划分多个进程呢?
    进程是资源管理的基本单元,创建进程时,一个比较独立的任务(事务)创建一个进程,这个事务尽量不与其他进程有太多的耦合性。

    2. 进程管理应用及资源

    (1)进程创建:进程创建于进程资源获取。fork/vfork

    在进程创建过程中:
    用户空间中:将程序的代码段,数据段,BBS段,从磁盘加载到内存,并且申请堆栈空间;
    内核空间中:为这个进程分配唯一的PID标识,同时在内核中为这个进程申请进程控制块PCB,初始化相关信息
    在运行的过程中,涉及打开的文件,关联的终端,安装信号,状态等系列信息
    在创建进程时,由父进程来创建子进程

           #include <unistd.h>
    
           pid_t fork(void);
    在父进程中,返回子进程的ID,在子进程中返回,返回0
    
    (2)进程中执行新的代码

    在进程执行过程中,要执行新代码,实际上是创建了一个新进程,更多的是期望在这个进程中执行新的代码,而不是原来的程序代码。使用exec相关的函数可以在子进程中,替换原有进程的代码段和数据段(用户空间的信息),转而执行新代码

           #include <unistd.h>
    
           extern char **environ;
    
           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[]);
    参数说明:
    path:可执行程序的路径
    arg:参数列表(以NULL结束)
    file:文件名(要求系统在$PATH环境变量所列路径下搜索)
    argv[]:执行这个程序的参数列表字符串指针数组
    
    用法参考:
    execl("/bin/ls", "ls", "-l", NULL);
    execlp("ls", "ls", "-l", NULL);
    char *const argv[] = {"ls", "-l", NULL};
    execv("/bin/ls", argv);
    
    (3)进程退出

    进程在以下几个情况会退出:

    • 使用kill函数 kill -9 pid:强制退出
    • 显示的执行exit系列函数
    NAME
           _exit, _Exit - terminate the calling process
    SYNOPSIS
           #include <unistd.h>
           void _exit(int status);
    
           #include <stdlib.h>
           void _Exit(int status);
    
    NAME
           exit - cause normal process termination
    SYNOPSIS
           #include <stdlib.h>
           void exit(int status);
    
    exit()与_exit()的区别:
    exit()在退出时会对进程资源做清理,例如刷新流流的缓冲区;
    _exit()不做任何处理,直接退出。
    
    • 进程遇到main函数的return或者遇到}没有代码可执行时退出

    exitreturn的区别:
    exit是一个系统函数,退出这个进程;
    return是C/C++关键字,退出这个函数。

    • 在退出时,我们可以在退出前注册退出时执行的代码
    NAME
           atexit - register a function to be called at normal process termination
    SYNOPSIS
           #include <stdlib.h>
           int atexit(void (*function)(void));
    
    NAME
           on_exit - register a function to be called at normal process termination
    SYNOPSIS
           #include <stdlib.h>
           int on_exit(void (*function)(int , void *), void *arg);
    

    注册回调函数,即执行exit()或者正常退出时去执行相应的回调函数代码,实际上是提供一个功能,在退出进程时完成相应的进程资源清理工作,相当于C++中的析构。

    (4)进程资源回收

    进程退出有退出的状态,且进程资源的回收在exit的时候仅仅释放了它的用户空间资源,而内核空间资源PCB没有回收,转而由它的父进程通过wait相关的函数来回收
    子进程在退出时会给父亲进程发出一个信号(SIGCHLD),父进程可以显示的调用wait/waitpid等待子进程结束并回收资源,回收资源时,也可以得到子进程退出的状态

    NAME
           wait, waitpid, waitid - wait for process to change state
    SYNOPSIS
           #include <sys/types.h>
           #include <sys/wait.h>
    
           pid_t wait(int *status); 
    参数status用来存储子进程退出状态,返回值为退出的子进程PID,
    这个函数以阻塞的方式等待某个进程退出,当进程退出后,此函数返回。
           pid_t waitpid(pid_t pid, int *status, int options);
    此函数为指定等待某个进程或者某些进程,其中第一个参数可以为以下值:
           < -1   meaning wait for any child process whose process group ID is equal to the absolute value of pid.
           -1     meaning wait for any child process.
           0      meaning wait for any child process whose process group ID is equal to that of the calling process.
           > 0    meaning wait for the child whose process ID is equal to the value of pid.
    第二个参数status用来存储子进程退出状态
    第三个参数options一般为0
    

    使用示例:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main()
    {
        pid_t pid = fork();
    
        if( pid < 0 )
        {
            perror("fork");
        }
        else if( pid == 0 ) 
        {
            sleep(1);
            printf("child ID = %d\n", getpid());
            exit(10);
        }
        else
        {
            int stat;
            pid_t w_pid;
    
        //  w_pid = wait(&stat);
            w_pid = waitpid(-1, &stat, 0);
    
            printf("status = %d, wait_pid = %d\n", stat>>8, w_pid);
        }
    
        return 0;
    }
    

    总结资源申请与释放的问题:
    创建时,申请进程的所有资源。内核空间中的PCB,用户空间中的加载了代码段,数据段,BSS段,申请了堆栈空间,打开的文件,安装的信号,关联的终端等;
    执行时,替代代码段,数据段,BSS段,堆栈空间等用户空间信息,但内核PCB的信号没有修改;
    退出时,释放了自己用户空间的资源
    回收时,回收内核空间资源。

    两个重要的概念:

    • 僵死进程:进程已经退出,但是内核空间资源没有回收的进程
    • 孤儿进程:父进程先于子进程退出,这样的子进程就是孤儿进程,其父进程会被转移到init(pid=1)进程

    3. 进程创建详解与父子进程资源

    (1)父子执行顺序问题

    父子进程在创建完子进程后互不相关联,以独立身份抢占CPU资源,具体谁先执行由调度算法决定,用户空间没办法干预。子进程执行代码的位置是fork/vfork函数返回的位置。

    (2)子进程资源申请问题

    子进程重新申请新的物理内存空间,复制父进程地址空间所有的信息(现在的操作系统实际采用写时复制等策略,真正的物理内存空间发生在需要写入时)
    子进程在用户空间中复制父进程的代码段,数据段,BSS段,堆,栈所有的信息在内核空间中操作系统为其重新申请一个PCB,并且使用父进程的PCB来初始化,除了pid特殊信息外,几乎所有的信息都是一样的

    • 父子进程中资源申请问题
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int glob = 100;
    
    int main()
    {
        pid_t pid = fork();
        int num = 20;
    
        if( pid < 0 )
        {
            perror("fork");
            exit(EXIT_FAILURE);
        }
        else if( pid == 0 ) 
        {
            num = 1000;
            glob = 2000;
            printf("child process: num = %d, glob =%d\n", num, glob);       // child process: num = 1000, glob =2000
            printf("child process: &num = %p, &glob =%p\n", &num, &glob);   // child process: &num = 0xbfe94ff8, &glob =0x804a028
        }
        else
        {
            sleep(1);
            printf("parent process: num = %d, glob =%d\n", num, glob);      // parent process: num = 20, glob =100
            printf("parent process: &num = %p, &glob =%p\n", &num, &glob);  // parent process: &num = 0xbfe94ff8, &glob =0x804a028
        }
    
        return 0;
    }
    

    观察输出结果:
    在子进程中先修改变量的值,并不影响父进程,明数据段,栈(当然也包括其它用户空间内存),子进程是申请新的物理空间;
    但从打印的地址来看,父子进程中的变量地址为同一个地址,这是为什么?
    这里打印的是虚拟地址,而不是物理地址编号;两个进程的虚拟地址空间是没有任何联系的。

    • 父子进程中文件流的缓冲区状态
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main()
    {
        printf("hello\nworld");
    
        fork();
        printf("bye\n");
    
        return 0;
    }
    
    ===============
    输出结果:
    hello
    worldbye
    worldbye
    

    流的缓冲区会缓存没有刷新的信息,且缓冲区在用户空间中,虽然子进程创建后从fork返回处执行,但缓冲区被子进程复制了一份,这里存储在缓冲区中的world也被复制了一份,因此,输出了两份world。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main()
    {
        for(int i=0; i<2; i++)
        {
            fork();
            printf("*");
        }
    
        return 0;
    }
    
    输出结果:
    ********
    
    输出过程详解

    4.子进程创建或执行execX后,对打开的文件的操作方式

    根据前面所学的内容,在同一个进程中,两次打开(使用open)同一个文件(只要没有对文件上锁),分别写入文件会存在覆盖的情况。
    而使用fcntl/dup复制文件描述,分别使用这两个文件描述符写文件并不会出现覆盖,而是交叉写入
    原因:两次open打开实际上在内核中创建了两个互不相关的文件表项(struct file),也就记录了两个读写位置。而复制文件描述符则在内核中使用同一个文件表项,因此,共用以一个读写位置。

    • 在子进程中是如何操作父进程中打开的文件?
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    int main()
    {
        int fd;
        int fd2;
    
        fd = open("test.txt", O_CREAT|O_RDWR);
    
        if( fd < 0 ) 
        {
            perror("open");
        }
    
        pid_t pid = fork();
    
        if( pid == -1 )
        {
            perror("fork");
            exit(EXIT_FAILURE);
        }
        else if(pid == 0)
        {
            write(fd, "helloworld", 10);
        }
        else
        {
            sleep(1);
            write(fd, "abc", 3);
        }
    
        close(fd);
    
        return 0;
    }
    
    输出结果:
    helloworldabc
    

    通过以上代码测试,父子进程共享一个文件表项(file struct),也就是共用一个读写位置。



    操作文件时,父子进程共用读写位置。

    • 在execX系列函数替换代码后,对打开的文件能够再处理吗?
      默认情况下,execX执行的代码可以访问在原来代码中打开的文件,操作是同一个文件描述符,即用一个文件对象
      fcntl(fd,F_SETFD,FD_CLOEXEC);
      语句用来使在execX之前打开的文件描述符在新的代码中不可用。

    5. 进程属性获取与修改

    进程的属性包括进程组属性用户属性

    (1)进程组属性决定了进程中运行过程中控制权限以及相关控制信息
    1. PID:进程号。当前进程在当前系统下唯一的编号,针对这个进程执行的操作多以进程号为标识:例如,等待某个子进程结束waitpid;向某个进程发送信号。用户可以获取getpid(),但不能通过函数修改进程号的值。
    2. PPID:父进程号。一般为创建这个进程的那个进程的ID,一般也不会修改,当这些进程的父进程退出后,当前进程变成孤儿进程,它的PPID会被修改成init进程,即PID=1的这个进程。
    3. PGID:进程组号。将完成协同工作的多个进程默认为一个进程组。例如,在终端运行一个新的程序,新的程序创建的子进程以及自身在一个进程组下,而第一个进程默认为进程组长,进程组号也是进程组长的ID。PGID可以被获取和修改,getpgid(pid_t pid):参数为某个进程的ID,返回该进程的进程组长编号;setpgid(pid_t pid, pid_t pgid):修改某个进程的进程组长。

    修改一个进程的进程组号的意义:kill可以向一个进程组发起信号,要影响整个这个进程组的所有进程。

    1. SID:会话ID(session ID)。会话:进行交互。一般,在某个终端下执行的程序所创建的进程/进程组,它们的SID就是这个终端的编号。在一个会话下的所有进程都受到这个会话终端的影响。
      getsid(pid):获取某个进程的会话ID。
      setsid():设置某个进程为会话组长,要求这个进程不能说进程组长。一般在创建守候进程时会修改SID,避免原来关联进程的终端信号影响子进程。
    2. 终端
      一个进程可以与某个终端关联,建立与控制终端关联的这个会话首进程为控制进程
      一个会话中的多个进程组可以分为一个前台和多个后台
      在终端下执行键盘命令ctrl+c等,会将信号发送给前台进程组所有进程。
    NAME
           tcgetpgrp, tcsetpgrp - get and set terminal foreground process group
    
    SYNOPSIS
           #include <unistd.h>
    
           pid_t tcgetpgrp(int fd);
    
           int tcsetpgrp(int fd, pid_t pgrp);
    
    (2) 用户属性决定了进程在运行时对其他资源的访问权限,如对文件的读写权限
    1. uid/ruid:创建这个进程的用户的ID。例如用户ID为500,uid就是为500
    2. gid/rgid:创建这个进程的用户所在组的id。
    3. EUID:有效用户ID,一般同uid。
    4. EGID:有效用户组ID,一般同gid。

    一般对文件真正的访问权限由EUID和EGID决定,当EUID和EGID仅仅是在这个可执行程序的setuid位和setgid位被设置时,相应的EUID和EGID将与执行这个进程的UID/GID不同,上升到了这个可执行程序的setuid用户。
    如:

    delphi@delphi-vm:~/code/linux_coding$ ll /usr/bin/passwd 
    -rwsr-xr-x 1 root root 37100 2011-02-15 06:12 /usr/bin/passwd*
    passwd的可执行程序setuid被置位,普通用户执行这个程序时,UID是普通用户ID,
    但EUID上升到了这个文件的拥有者root,即这个进程对文件的访问权限为root用户的权限。
    因此可以修改/etc/passwd这个文件。
    

    相关文章

      网友评论

          本文标题:Linux高级环境编程之7执行单元_进程管理

          本文链接:https://www.haomeiwen.com/subject/alalbftx.html