美文网首页
Linux的进程, since 2020-11-01

Linux的进程, since 2020-11-01

作者: Mc杰夫 | 来源:发表于2020-11-01 12:13 被阅读0次

    进程Process

    进程和程序的关系

    程序规定了活动的动作,但应用程序不等于进程(process)。进程是程序的一个具体实现,只有运行程序,才能产生一个进程。程序与进程,类似于食谱和做菜的关系。进程是执行程序的过程。在Linux中用ps来查询正在运行的进程。

    $ps -eo pid,cmd #-e表示列出全部进程,-eo pid,cmd表示需要的信息
    

    上面指令的返回结果中,有的CMD名字用[]中括号包围起来的,是内核的一部分功能,被打扮成进程的样子方便操作系统管理。PID为1的进程一定是由/sbin/init程序运行而形成 (在MacOS中为/sbin/launchd)。

    操作系统的一个重要功能就是管理进程,为进程提供必要的计算机资源,如内存空间,管理进程的相关信息。

    进程的生成: fork机制

    当Linux启动,init是系统内核创建的第一个进程也是唯一一个进程,它会一直存在直到关闭计算机。其他的进程,如音乐播放器Shell等等除了init外的所有进程,都是通过fork机制创建。fork为“分叉”,从老进程中复制出一个新进程。老进程分出新进程后,老金城继续运行,称为新进程的父进程(parent process),新进程称为老进程的子进程(child process)。在Shell中,进程ID称为PID,父进程ID称为PPID。用下面指令查询当亲Shell下的进程和其父进程。

    $ps -o pid,ppid,cmd
    

    任何进程,沿着PPID循迹而上,都会发现源头是init,查询结果显示,绝大多数进程的PPID都是1。Linux的所有进程构成了一个树状结构,该树以init为根,用pstree显示树莓派的整个进程树。

    fork系统调用

    通过fork系统调用生成新的进程,调用发生后就有父与子两个进程,而两者的进程空间完全相同

    系统如何知道自己是父与子中的哪一个?
    fork之后有两个进程,fork系统调用会返回两次。

    一次返回到父进程,把子进程的PID作为返回值交给父进程。如果fork不成功,那么fork调用就会返回一个负值给父进程。

    另一次返回到子进程,用0作为返回值。检察fork调用的返回值,如果是0,进程就知道自己是子进程。父进程通过fork返回值知道子进程的PID,进程通过PPID知道自己的父进程,进程们因此知道自己在进程树中的位置。

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    int main(void){
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        printf('fork error\n');
        exit(1);
    } else if (pid == 0) {
        /*子进程*/
        printf('child: %d\n',getpid()); //getpid用于获取当前进程的PID
        sleep(1);
    } else {
        /*父进程*/
        printf('parent: %d\n', getpid());
        sleep(2);
      }
    }
    

    进程通过fork返回弄清了自己是子或父进程,就能根据情况执行不同的任务。获知自己是子进程后,可通过exec系统函数中的一个来加载新的程序文件,从而与父进程执行不同的任务。比如Shell执行ls指令,会先fork自己的进程,然后在子进程中运行/bin/ls这个程序文件。

    资源fork

    进程空间记录进程的数据和状态。当进程fork时,Linux需要在内存中分配新的进程间给新进程。进程空间还记录了进程的转台和数据。原有进程空间的所有内容,如程序段、全局数据、栈和堆,都要复制到新的进程空间中。

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void) {
    pid_t pid;
    int var = 1024;
    pid = fork();
    if (pid <0) {
        printf('fork error\n');
        exit(1);
    } else if (pid == 0) {
        /*子进程*/
        printf('child: %d\n', var);
    } else {
        /*父进程*/
        sleep(1);
        printf('parent: %d\n', var);
        }
    return 0;
    }
    

    除了空间,fork还会复制进程描述符(process descriptor, pd)。内核中保存了每个进程的相关信息,即pd。每个进程都在内核中有一个对应的pd。PID、PPID、信号,都在pd中。

    fork之后系统出现了新的进程,内核要增加对应进程的pd。child从parent继承的内容(即相同信息)包括:

    • 当前工作目录 pwd
    • 环境变量 env var
    • 已经打开的文件的相关信息
    • 信号mask和disposition

    parent和child有的信息不同

    • PID/PPID
    • 进程运行的相关信息在子进程中重置为0
    • parent的文件锁在child中清空
    • parent的未处理信号在child中被清空

    这些信息都是描述进程个体特征的信息。

    最小权限原则 least privilege

    Linux中的该原则,收缩进程所享有的权限,以防进程滥用特权。进程权限也是根据用户身份进行分配。进程的不同阶段可能需要不同的特权。进程与身份挂钩,意味着进程需要在不同身份之间变化。

    三个身份:真实身份、存储身份和有效身份。每个身份含有UID(user?)和GID(group?)。真实身份是用户登录使用的身份;存储身份如果设置,是程序文件的拥有者;有效身份是判断进程权限时使用的身份。

    进程运行中,可从真实身份和存储身份中选择一个,复制到有效身份。并不是所有的程序都需要设置存储身份。需要这么做的程序文件会把权限的执行位上的x改为s。这时,用户权限的这一位叫做设置UID位(set UID bit),而组权限的这一位叫做设置GID位(set GID bit)

    进程终结

    进程终结的情况

    • main函数结尾调用return
    • 程序中的某个位置调用exit函数退出
    • 根据信号终结
    • 进程出现致命错误,如进程出现栈溢出

    进程终结时有一个退出码,正常退出时,退出码为0。有错误或者异常退出,退出码是大于0的整数。根据退出码可以得知进程退出的原因。

    进程终结时,父进程会获得通知,进程空间随机被清空,而进程附加信息会保留在内核空间中。一个进程终结了,它会在内核中留下痕迹,删除进程对应内核信息的任务,由父进程完成。

    按Linux惯例,父进程有义务对子进程使用wait系统调用。调用wait之后,父进程暂停,等待子进程终结。子进程终结后,父进程从内核中取出子进程的退出信息,并清除子进程的进程描述符pd。完成上述工作,父进程恢复运行。下面是wait程序的demo。

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    int main() {
        pid_t pid = fork();
        if (pid < 0) //无法创建子进程
        { printf('error'); }
        else if (pid == 0) //子进程
        { //do some calculation
            sum = 0
            for (int i = 0; i < 10000000; i++) {
                sum += i;
            }
            exit(0);
        }
    else //父进程
    { int status;
      printf('child PID %d...\n', pid);
      //等待子进程结束
      do {
            waitpid(pid, &status, WUNTRACED);
      } while (!WUNTRACED(status) && !WIFSIGNALED(status));
      //status是子进程的返回值
      printf('child return value %d\n', status);
    }
    }
    

    如果父进程早于子进程终结,child就会和进程树失联,成为孤儿进程(orphand process),此进程会称为init的子进程,因此init是所有孤儿进程的parent,完成wait调用。但wait系统调用只是约定俗称的责任,不是Linux强制,一个程序完全可以不对子进程调用wait,但这会导致子进程的退出信息滞留在内核中,此时子进程成为僵尸进程(zombie process),僵尸进程累计,内核空间会被挤占,应避免发生。

    Reference

    1 Vamei,周昕梓著,树莓派开始玩转Linux,中国工信出版集团,电子工业出版社

    相关文章

      网友评论

          本文标题:Linux的进程, since 2020-11-01

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