精华内容
下载资源
问答
  • 进程
    千次阅读 多人点赞
    2021-11-04 22:49:19

    一、守护进程概述

      守护进程是一个生存期较长的进程,他常常在系统引导装入是启动,仅仅在系统关闭的才终止。也就是通常所说的 Daemon 进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 Linux 中很多系统服务都是通过守护进程实现的。

      在Linux中,可以根据指令 ps 命令打印进程的状态,在终端输入指令ps -ajx 可以查看当前进程的状态,如下所示(已删除部分内容)。

    ubuntu@songshuai:~$ ps -ajx 
     PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
        0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init splash
        0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
        2     3     0     0 ?           -1 I        0   0:00 [kworker/0:0]
        2     4     0     0 ?           -1 I<       0   0:00 [kworker/0:0H]
        2     5     0     0 ?           -1 I        0   0:00 [kworker/u128:0]
        2     6     0     0 ?           -1 I<       0   0:00 [mm_percpu_wq]
        2     7     0     0 ?           -1 S        0   0:00 [ksoftirqd/0]
        2     8     0     0 ?           -1 I        0   0:00 [rcu_sched]
        2     9     0     0 ?           -1 I        0   0:00 [rcu_bh]
        1  1215  1215  1215 ?           -1 SLsl     0   0:00 /usr/sbin/lightdm
        1  1782  1782  1782 tty1      1782 Ss+      0   0:00 /sbin/agetty --noclear tty1 linux
     1763  1864  1863  1863 ?           -1 S     1000   0:00 upstart-udev-bridge --daemon --user
     1763  1874  1874  1874 ?           -1 Rs    1000   0:00 dbus-daemon --fork --session --address=unix:abstract=/tmp/dbus-den4cotw4
     1763  1886  1886  1886 ?           -1 Ss    1000   0:00 /usr/lib/x86_64-linux-gnu/hud/window-stack-bridge
     1763  1911  1910  1910 ?           -1 S     1000   0:00 upstart-dbus-bridge --daemon --system --user --bus-name system
     1763  1913  1912  1912 ?           -1 S     1000   0:00 upstart-dbus-bridge --daemon --session --user --bus-name session
     1763  1917  1916  1916 ?           -1 S     1000   0:00 upstart-file-bridge --daemon --user
     1763  1919  1918  1918 ?           -1 Sl    1000   0:04 /usr/bin/fcitx
     1763  1943  1943  1943 ?           -1 Ssl   1000   0:00 /usr/lib/x86_64-linux-gnu/bamf/bamfdaemon
     2589  2594  2594  2594 pts/2     2677 Ss    1000   0:00 bash
     2594  2677  2677  2594 pts/2     2677 R+    1000   0:00 ps -ajx
    ubuntu@songshuai:~$ 
    

      在 ps 输出示例中,内核守护进程的名字出现在方括弧中,改版本的 Linux 使用一个名为 kthreadd 的特殊内核进程来创建其他内核进程,所以 kthreadd 表现为其他内核进程的父进程。

      进程 init 的进程通常为 1,他是一个系统守护进程,除了其他工作外,主要负责各个运行层次特定的系统服务。

      在Linux 中,每一个系统与用户进行交流的界面称为 终端。每一个从此终端开始运行的进程都会依附于该终端,这个终端称为这些进程的 控制终端。当控制终端关闭时,相应的进程都会自动结束。但是守护进程却能够突破这种限制,不受终端关闭的影响。反之,如果希望某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个 守护进程

    二、守护进程创建

      创建守护进程,需要遵循特定的流程,以防产生不必要的交互过程。下面就分几个步骤来创建一个简单的守护进程。

    2.1、创建子进程,父进程退出

      由于守护进程是脱离控制终端的,因此完成第一步后子进程变成后台进程。之后的所有工作都在子进程中完成。而用户通过 shell 可以执行其他的命令,从而在形式上做到了与控制终端的脱离。

      另外一方面虽然子进程继承了父进程的进程组 ID,但获得了一个新的进程 ID,这样保证了子进程不是一个进程组的组长进程。这也是下面要进程的第三步的先决条件。

    说明:
      由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程(孤儿进程可以查看博文:Linux – 多进程编程之 - 基础实现、孤儿进程)。在 Linux 中,每当系统发现一个孤儿进程,就会自动由 1号进程(也就是 init 进程)收养它,这样,原先的子进程就会变成 init 进程的子进程了。

    2.2、在子进程中创建新会话

      这个步骤是创建守护进程中最重要的一步,在这里使用的函数是 setsid()

    2.2.1、进程组和会话期

      这里先要明确两个概念:进程组和会话期。

    进程组
      进程组是一个或多个进程的集合。进程组由进程组 ID 来唯一标识。除了进程号( PID )之外,进程组 ID 也是一个进程的必备属性。
    每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID ,且进程组 ID 不会因组长进程的退出而受到影响。

    会话期
      会话组是一个或多个进程组的集合。通常一个会话开始于用户登录,终止于用户退出;或者说开始于终端打开,结束于终端关闭。会话期的第一个进程称为会话组长。在此期间该用户运行的所有进程都属于这个会话期。

      进程组和会话期之间的关系如图2.1所示。

    图2.1 进程组和会话期之间的关系图

    2.2.2、setsid()函数说明

    1、setsid()函数原型

      setsid()函数原型如下所示(使用指令 man 2 setsid 即可显示如下代码)。

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

    功能:
      如果调用进程不是进程组长,则 setsid() 将创建一个新会话。调用进程将成为新会话的会话组组长(即,其会话 ID 与其进程 ID 相同)。同时调用进程也将成为会话中新进程组的进程组组长(即,其进程组 ID 与其进程 ID 相同)。调用进程将是新进程组和新会话中的唯一进程。
    参数:无
    返回:
      成功:返回调用进程的(新)会话ID
      失败:返回(pid_t)-1,并设置 errno

    2、setsid()函数作用

      上面已经提到,setsid() 函数用于创建一个新的会话,并担任该会话的组长,所以调用 setsid() 有下面 3 个作用。

    1、让进程摆脱原会话的控制
    2、让进程摆脱原进程组的控制
    3、让进程摆脱原控制终端的控制

      由于在调用 fork() 函数时,子进程 全盘复制 了父进程的会话期进程组和控制终端等。所以虽然父进程退出了,但原先的 会话期进程组控制终端等并没有改变,因此,子进程并不是真正意义上的独立,而 setsid() 函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

    2.3、改变当前工作目录

      使用 fork() 函数创建的子进程是完全继承了父进程的当前工作目录,所以从父进程继承过来的当前工作目录可能是一个挂载的文件系统中。因为守护进程有一般情况是在系统在引导之前是一直从在的,所以在进程工作的过程中当前目录所在的文件系统(比如“/mnt/usb” 等)是不能卸载的。

      因此,一般的做法是将根目录作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如“/tmp”

      改变工作目录的函数是 chdir() 函数,其函数原型如下所示。

    #include <unistd.h>
    
    int chdir(const char *path);
    

    功能:
      改变调用者的工作目录
    参数:
      path:新的工作目录的路径
    返回:
      成功:返回0
      失败:返回-1,同时设置errno

    2.4、重设文件权限掩码

      文件权限掩码(通常用八进制表示)的作用是屏蔽文件权限中的对应位。例如,如果文件权限拖码是050,它表示屏蔽了文件组拥有者的可读与可执行权限。由于使用 fork() 函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了一定的影响。如果守护进程需要创建文件,那么他可能需要设置特定的权限。因此,把文件权限掩码设置为一个已知的值(通常设置为0),可以增强该守护进程的灵活性。

      设置文件权限掩码的函数是 umask()。在这里,通常的使用方法为 umask(0)。其函数原型如下所示。

    #include <sys/types.h>
    #include <sys/stat.h>
    
    mode_t umask(mode_t mask);
    

    功能:
      umask() 将调用进程的文件模式创建掩码( umask)设置为 mask & 0777(即仅使用掩码的文件权限位)。
    参数:
      mask:要设置的权限值,用八进制表示
    返回:
      此系统调用始终成功,并返回掩码的上一个值。

    2.5、关闭不需要的文件描述符

      同样地,用 fork() 函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程访问,但它们一样占用系统资源,而且还可能导致所在的文件系统无法被卸载。

      特别是守护进程和终端无关,所以指向终端设备的标准输入、标准输出和标准错误流等已经不再使用,应当被关闭。

      可以使用函数 getdtablesize() 来获取当前金成文件描述符表的大小,并通过使用 close() 来依次关闭。

    函数原型如下。

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

    getdtablesize()函数返回进程可以打开的最大文件数,比文件描述符的最大可能值多一个。

    #include <unistd.h>
    int close(int fd);
    

    close() 用于关闭文件描述符,关闭成功则返回 0,失败则返回 -1 并设置 errno

      所以关闭文件描述符的代码可以如下写法。

    int num = getdtablesize(); // 获取当前进程文件描述符表大小
    
    for (int i = 0; i < num; i++)  
    {
        close (i);
    }
    

    2.6、某些特殊的守护进程打开/dev/null

      某些特殊的守护进程打开/dev/null,使其具有文件描述符0、1、2,这样任何一个试图读标准输入、标准输出、标准出错时都不会有任何效果,这样符合了守护进程不与终端设备相关联的属性。

      综上所述,一个守护进程就可以创建成功了,所以创建守护进程的流程可以总结为如图2.2所示。

    图2.2 创建守护进程流程图

    三、守护进程代码示例

      用下面的一个简单的程序,用来示例守护进程的完整的创建过程。守护进程的主要工作则是每隔一定时间向日志文件“/daemon.log”文件中写入内容。

    #include <fcntl.h>     // for O_APPEND ..
    #include <stdio.h>     // for perror ..
    #include <stdlib.h>    // for exit ..
    #include <string.h>    // for strlen
    #include <sys/stat.h>  // for umask
    #include <sys/types.h> // for setsid
    #include <unistd.h>    // for setsid
    
    int main(int argc, const char *argv[])
    {
        pid_t pid = 0;
        int i = 0;
        char *filePath = "daemon.log";
    
        /* 第一步:创建子进程,父进程退出 */
        pid = fork();
        if (pid == -1) /* fork出错 */
        {
            perror("fork error");
            exit(EXIT_FAILURE);
        }
        else if (pid == 0) /* 子进程 */
        {
            pid_t temp_pid = 0;
            /* 周期计数的变量 */
            int cycleCnt = 0;
            /* 第二步:创建新的会话 */
            temp_pid = setsid();
            /* 第三部:改变当前的工作路径*/
            chdir("/");
            /* 第四步:改变进程本身的umask */
            umask(0);
            /* 第五步:关闭所有可能已打开的文件描述符 */
            int num = getdtablesize(); /* 获取当前进程文件描述符表大小 */
            for (i = 0; i < num; i++)
            {
                close(i);
            }
    
            /* 至此,守护进程创建完成,以下正式开始守护进程的工作 */
            /* 1、打开要操作的文件 */
            int fd = open(filePath, O_RDWR | O_CREAT | O_APPEND, 0600);
            if (fd == -1) /* open操作失败 */
            {
                perror("open error");
                exit(EXIT_FAILURE);
            }
    
            /* 2、在文件中循环写入测试数据 */
            while (1)
            {
                /* 写入内容的缓冲区定义 */
                char writeBuff[128] = {0};
                /* 周期运行计数自加 */
                cycleCnt++;
                /* 写入的数据拼接 */
                sprintf(writeBuff, "I'm Daemon Process, Running %d\n", cycleCnt);
                /* 写入到文件中 */
                write(fd, writeBuff, strlen(writeBuff));
                /* 休眠片刻 */
                sleep(1);
            }
        }
        else /* 父进程 */
        {
            /* 父进程退出 */
            exit(EXIT_SUCCESS);
        }
    
        return 0;
    }
    
    

      编译并运行上述程序,运行效果及进程状态如图3.1所示。

    图3.1 运行效果及进程状态效果图

      需要注意的是,因为守护进程的工作目录已经修改为“\”,并且程序中需要在根目录中创建文件并写入,所以需要root权限,运行程序使用 sudo 命令(下同)。

      此时查看程序记录的文件,使用指令sudo cat daemon.log 即可显示,文件内容如图3.2所示。

    图3.2 文件内容显示效果图

      至此为止,守护进程相关的主要知识基本上总结的差不多了。基于守护进程的特征,那么在记录守护进程的异常处理方面也需要特殊的处理,那么将会下一篇进程守护进程的出错的处理。

      好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然 关注一波 那就更好了,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。

    上一篇:Linux – 多进程编程之 - 僵尸进程
    下一篇:Linux – 多进程编程之 - 守护进程的出错处理

    更多相关内容
  • 【操作系统】第五话·进程的切换与进程控制

    千次阅读 多人点赞 2022-04-02 10:01:30
    进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。简而言之,进程控制就是为了实现进程状态转换。 2.如何实现进程控制? 我们使用“原语”来实现...

    🌕写在前面


    Hello🤗大家好啊,我是kikokingzz,名字太长不好记,大家可以叫我kiko哦~

    从今天开始,我们将要开启一个新的系列【闪耀计划】,没错!这是今年上半年的一整个系列计划!本专题目的是通过百天刷题计划,通过题目和知识点串联的方式完成对计算机操作系统的复习和巩固;同时还配有专门的笔记总结和文档教程哦!想要搞定,搞透计算机操作系统的同学,本专栏将会通过模块化的分类,刷够1000道题,为大家提供点对点的考点相关知识轰炸!值得注意的是,本专栏将会通过教程+课后习题的方式来进行巩固教学,课后习题的题量也是算入总题数的哦!

    🎉🎉订阅本专栏,将为你带来最一手的备战秘籍!🎉🎉

    🍊博客主页:kikoking的江湖背景🍊


    🌟🌟往期必看🌟🌟

    🔥【闪耀计划】第一话· 操作系统的基本概念

    🔥【闪耀计划】第二话·现代操作系统的前世今生

    🔥【闪耀计划】第三话·操作系统运行环境的层层细节

    🔥【操作系统】第四话·进程和程序究竟有啥区别?

    目录

    🌕写在前面

    🍺知识点7:进程的状态与切换

    🥝7.1 进程的状态

    🍊1.进程有哪几种状态呢?

    🍊2.就绪态和等待态有什么区别呢?

    🍊3.上表中为什么要将处理机和其他资源划分开呢?

    🥝7.2 进程的状态切换

    🍊1.进程状态是如何变化的?

    📜习题检测

    🥝7.3 就绪队列与阻塞队列

    🍊1.什么是就绪队列和阻塞队列?

    🍊2.进程的链接方式有哪些?

    📜习题检测

    🍺知识点8:进程控制

    🥝8.1 进程控制的宏观解读

    🍊1.什么是进程控制?

    🍊2.如何实现进程控制?

    🍊3.为什么进程控制过程要一气呵成?

    🍊4.原语是如何做到“一气呵成的”?

    🥝8.2 进程控制之进程创建

    🍊1.父进程与子进程

    🍊2.操作系统创建新进程的过程

    🍊3.引起进程创建的事件有哪些呢?

    📜习题检测

    🥝8.3 进程控制之进程终止

    🍊1.操作系统终止进程的过程

    🍊2.引起进程终止的事件有哪些呢?

    🥝8.4 进程唤醒与阻塞

    🍊1.什么是进程的阻塞?

    🍊2.进程是如何阻塞的呢?

    🍊3.如何保护step2中的现场呢?

    🍊4.什么是进程的唤醒?

    🍊5.进程是如何被唤醒的呢?

    📜习题检测

    🌕写在最后

    热爱所热爱的, 学习伴随终生,kikokingzz与你同在!❥(^_-)

    🍺知识点7:进程的状态与切换

    🥝7.1 进程的状态


    🍊1.进程有哪几种状态呢?

    进程在生命周期内,通常有以下5中状态,前3种是进程的基本状态:

    1.运行态进程正在处理机上运行;对于单处理机,同一时刻只有一个进程处于运行态。

    2.就绪态进程获得了除处理机以外的一切所需资源,一旦得到处理机便可立即运行。

    3.阻塞态又称等待态,进程正在等待某一事件而暂停运行,如等待某资源或IO完成,即使处理机空闲,该进程也不能运行。

    4.创建态进程正在被创建,尚未转到就绪态。

    5.终止态进程正从系统中消失,可能是进程正常结束或其他原因退出运行。


    🍊2.就绪态和等待态有什么区别呢?

    就绪态等待态
    处理机资源只缺处理机可能缺;也可能不缺
    其他资源已获得所需资源等待某资源可用或等待一件事情完成
    当获得处理机时立即运行即使处理机空闲,当等待的事情没有完成,仍无法运行

    🍊3.上表中为什么要将处理机和其他资源划分开呢?

    因为在分时系统的时间片轮转机制中,每个进程分到的时间片是若干毫秒。也就是说,进程得到处理机的时间很短且非常频繁,进程在运行过程中是频繁地转换到就绪态的;而其他资源的使用(如外设的使用和分配,或某一件事情的发生,如I/O操作的完成对应的时间)对应的时间相对很长,因此将它们划分开。

     ✨✨✨我是分割线✨✨✨

    🥝7.2 进程的状态切换


    🍊1.进程状态是如何变化的?

    1.就绪态——>运行态

    处于就绪态的进程被调度后,获得处理机资源,于是进程由就绪态切换为运行态。

    2.运行态——>就绪态

    情况1:处于运行态的进程在时间片用完后,不得不让出处理机,进而转换为就绪态。

    情况2:在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程序将正在执行的进程转换为就绪态,让更高优先级的进程执行。

    3.运行态——>阻塞态(主动行为)

    进程请求某一资源(如外设)的使用或等待某一事件的发生(如I/O操作的完成)时,它就从运行态转换为阻塞态。

    进程以系统调用的形式请求操作系统提供服务,这是一种特殊的,由用户态程序调用操作系统内核过程的形式。

    4.阻塞态——>就绪态(被动行为:需要其他相关进程的协助)

    进程等待的事件到来,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞态转换为就绪态。

    📜习题检测


    📜071.题目难度 ⭐️

    071.进程自身决定( )。
    A.从运行态到阻塞态 
    B.从运行态到就绪态 
    C.从就绪态到运行态 
    D.从阻塞态到就绪态

    🍊详细题解:

    A. 由上图可知只有运行态到阻塞态到转换是由进程自身决定的。

    B. 从运行态到就绪态的转换是由于进程时间片用完或者被高优先级进程剥夺。

    C. 进程从就绪态到运行态的转化是被动调度的。

    D. 从阻塞态到就绪态的转化是由协作进程决定的。

    ✅正确答案:A


    📜072.题目难度 ⭐️

    072.在任何时刻,一个进程的状态变化( )引起另一个进程的状态变化。 
    A.必定 
    B.一定不 
    C.不一定 
    D.不可能

    🍊详细题解:

    情况1.一个进程的状态变化可能会引起另一个进程的状态变化:当一个位于运行态的进程时间片用完,可能会引起另一个位于就绪态的进程运行。

    情况2.一个进程的状态变化可能不会引起另一个进程的状态变化:当一个位于阻塞态的进程切换到就绪态时,不会引起其他进程的状态变化。

    鉴于上述两种情况都有可能发生,因此本题应选择“不一定”会引起另一进程的状态变化。

    ✅正确答案:C


    📜073.题目难度 ⭐️⭐️

    073.一个进程释放了一台打印机,它可能会改变( )的状态。 
    A.自身进程 
    B.输入/输出进程 
    C.另一个等待打印机的进程 
    D.所有等待打印机的进程

    🍊详细题解:

    由于打印机是临界资源,一次只能供一个进程使用,因此当一个进程释放了一台打印机,另一个等待打印机的进程就可能从阻塞态转到就绪态。

    对于A选项来说,一个进程执行完毕后由运行态转为终止态时也有可能释放打印机,但该进程自身的状态转换并不是因为释放打印机造成的;相反是因为自身进程状态发生了转换,才释放了打印机。

    ✅正确答案:C


    📜074.题目难度 ⭐️⭐️

    074.系统进程所请求的一次IO操作完成后,将使进程状态从( )。 
    A.运行态变为就绪态 
    B.运行态变为阻塞态 
    C.就绪态变为运行态 
    D.阻塞态变为就绪态

    🍊详细题解:

    IO操作完成之前,进程在阻塞态等待这一事件完成;当所请求的IO操作完成后,进程等待⌚️就绪,变为就绪态。

    ✅正确答案:D


    📜075.题目难度 ⭐️⭐️

    075.一个进程的基本状态可以从其他两种基本状态转变过去,这个基本的状态一定是( )。
    A.执行状态 
    B.阻塞态 
    C.就绪态 
    D.完成状态

    🍊详细题解:

    从上图中我们不难发现,就绪态既可以由运行态转变回去,又可以由阻塞态转变回去:当需要的资源到达时,进程由阻塞态切换为就绪态;当时间片用完或被高优先级进程剥夺时,进程由运行态切换为就绪态。

    ✅正确答案:C


    📜076.题目难度 ⭐️⭐️⭐️

    076.支持多道程序设计的操作系统在运行过程中,不断地选择新进程运行来实现CPU的共享,
    但其中( )不是引起操作系统选择新进程的直接原因。
    A.运行进程的时间片用完 
    B.运行进程出错 
    C.运行进程要等待某一事件发生 
    D.有新进程被创建进入就绪态

     🍊详细题解:

    A. 此时该进程由运行态切换为就绪态,操作系统调度新的进程从就绪态切换为运行态。

    B. 运行进程出错,操作系统会选择新的进程到处理机运行。

    C. 此时该进程由运行态切换为阻塞态,if处理机空闲,操作系统选择新进程送处理机运行。

    D. 与操作系统选择新进程无关,创建新进程进入就绪态只会导致就绪队列+1

    ✅正确答案:D


    📜077.题目难度 ⭐️⭐️

    077.在进程转换时,下列( )转换是不可能发生的。
    A.就绪态→运行态
    B.运行态→就绪态
    C.运行态→阻塞态
    D.阻塞态→运行态

    🍊详细题解:

    由上图我们不难发现,阻塞态无法直接切换到运行态,其必须要经过 阻塞态--->就绪态--->最终才能转换到运行态。

    ✅正确答案:D


    📜078.题目难度 ⭐️⭐️⭐️

    078.进程处于( )时,它处于非阻塞态。
    A.等待从键盘输入数据
    B.等待协作进程的一个信号
    C.等待操作系统分配CPU时间
    D.等待网络数据进入内存

    🍊详细题解:

    阻塞态又称等待态,进程正在等待某一事件而暂停运行,这样的事件一般是:等待IO操作完成,如键盘;或是因互斥或同步数据引起的等待,如等待信号或等待进入互斥临界区代码段等;等待网络数据进入内存(为了进程同步);即使处理机空闲,该进程也不能运行。

    A. 进程正在等待IO操作,处于阻塞态。

    B. 进程正在等待某信号而暂停运行,位于阻塞态。

    C. 等待CPU调度的进程是位于就绪态的。

    D. 等待网络数据进入内存,位于阻塞态。

    ✅正确答案:C


    📜079.题目难度 ⭐️⭐️⭐️

    079.( )必会引起进程切换。
    A.一个进程创建后,进入就绪态
    B.一个进程从运行态变为就绪态
    C.一个进程从阻塞态变为就绪态
    D.以上答案都不对

    🍊详细题解:

    进程切换是指CPU调度不同的进程执行,当一个进程从运行态变为就绪态时,CPU调度另一个进程执行,引起进程切换。

    ✅正确答案:B


    📜080.题目难度 ⭐️⭐️

    080.【2015统考真题】下列选项中,会导致进程从执行态变为就绪态的事件是( )。
    A.执行P(wait)操作
    B.申请内存失败
    C.启动IO设备
    D.被高优先级进程抢占

    🍊详细题解:

    由上图可知,从运行态切换到就绪态的事件一共只有两种:第一种是时间片用完;第二种就是被更高优先级的进程剥夺,那么本题只有选项D满足要求。

    ✅正确答案:D


    📜081.题目难度 ⭐️⭐️⭐️

    081.【2018统考真题】下列选项中,可能导致当前进程Р阻塞的事件是( )。
    I.进程Р申请临界资源   
    II. 进程Р从磁盘读数 
    III.系统将CPU分配给高优先权的进程
    A.仅I                 B.仅Ⅱ              C.仅I、II             D. I、II、II

    🍊详细题解:

    阻塞态又称等待态,进程正在等待某一事件而暂停运行,这样的事件一般是:等待IO操作完成,如键盘;或是因互斥或同步数据引起的等待,如等待信号或等待进入互斥临界区代码段等;等待网络数据进入内存(为了进程同步);即使处理机空闲,该进程也不能运行。

    I.   进程正在等待某一临界资源的申请成功,处于阻塞态。

    II.  进程正在等待输入/输出操作完成,处于阻塞态。

    III. 进程此时被更高优先级的进程剥夺了处理机资源,由运行态切换为就绪态。

    因此导致当前进程阻塞的事件应当是I、II。

    ✅正确答案:C

     ✨✨✨我是分割线✨✨✨

    🥝7.3 就绪队列与阻塞队列


    🍊1.什么是就绪队列和阻塞队列?

    就绪队列系统中处于就绪状态的进程可能有多个,通常把它们排成一个队列。

    阻塞队列系统通常将处于阻塞态的进程也排成一个队列,甚至根据阻塞原因不同,设置多个阻塞队列。

    🍊2.进程的链接方式有哪些?

    进程的链接方式主要有两种:(1)链接方式;(2)索引方式

    链接方式:按照进程状态将PCB分为多个队列,操作系统持有指向各个队列的指针。

    索引方式:根据进程状态的不同,建立几张索引表,操作系统持有指向各个索引表的指针。

    📜习题检测


    📜082.题目难度 ⭐️⭐️

    082.进程创建完成后会进入一个序列,这个序列称为( )。
    A.阻塞队列
    B.挂起序列
    C.就绪队列
    D.运行队列

    🍊详细题解:

    我们要考虑创建进程的过程,当该进程所需资源分配完成只等CPU时,进程的状态此时为就绪态,因此所有就绪的程序控制块(PCB)一般以链表方式链接成一个序列,称为就绪队列。

    ✅正确答案:C


    📜083.题目难度 ⭐️⭐️⭐️

    083.在一个多道系统中,若就绪队列不空,就绪的进程数目越多,处理器的效率( )。
    A.越高
    B.越低
    C.不变
    D.不确定

    🍊详细题解:

    我们发现只要就绪队列不空,CPU就总是可以调度进程运行,保持繁忙,这与就绪进程的数目没有关系;除非就绪队列为空,此时CPU进入等待态,CPU效率下降。

    本题中提到“若就绪队列不空”,在该前提下,说明CPU总是有就绪进程可以调度,保持繁忙,因此就绪进程的数目不会影响处理机效率。

    ✅正确答案:C


    📜084.题目难度 ⭐️⭐️

    084.【2010统考真题】下列选项中,降低进程优先级的合理时机是( )。
    A.进程时间片用完
    B.进程刚完成IO操作,进入就绪队列
    C.进程长期处于就绪队列
    D.进程从就绪态转为运行态

    🍊详细题解:

    A. 进程时间片用完,可降低其优先级让其他进程被调度入执行状态。

    B. 此时进程由阻塞态进入就绪队列等待处理机调度,为了让其尽快处理I/O操作的结果,因提高优先级。

    C. 进程长期处于就绪队列,为使其不产饥饿现象,应适当提高优先级。

    D. 进程刚刚从就绪态转为运行态开始运行,不应该此时降低,应在时间片用完后再降低。

    ✅正确答案:A

    🍺知识点8:进程控制

    🥝8.1 进程控制的宏观解读


    🍊1.什么是进程控制?

    进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。简而言之,进程控制就是为了实现进程状态转换

    🍊2.如何实现进程控制?

    我们使用“原语”来实现进程控制,我们一般把进程控制使用的程序段称为原语,原语的特点是执行期间不允许中断,是一气呵成的,它是一个不可分割的基本单位。

    🍊3.为什么进程控制过程要一气呵成?

    如果进程控制过程中不能“一气呵成”,就有可能导致操作系统中的某些关键数据结构信息不统一,这会影响操作系统进行其他管理工作,比如下面👇这个例子:

    🍊4.原语是如何做到“一气呵成的”?

    原语的执行具有原子性,即执行过程只能一气呵成,期间不允许被中断;它使用“关中断指令“和”开中断指令“这两个特权指令实现原子性。

    ✨✨✨我是分割线✨✨✨

    🥝8.2 进程控制之进程创建


    🍊1.父进程与子进程

    允许一个进程创建另一个进程,此时创建者称为父进程,被创建的进程称为子进程。子进程可以继承父进程所拥有的资源;当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程;当父进程被撤销时,通常也会同时撤销其所有的子进程。


    📜085.题目难度 ⭐️⭐️

    085.【2020统考真题】下列关于父进程与子进程的叙述中,错误的是( )。
    A.父进程与子进程可以并发执行
    B.父进程与子进程共享虚拟地址空间
    C.父进程与子进程有不同的进程控制块
    D.父进程与子进程不能同时使用同一临界资源

    🍊详细题解:

    A. 父进程与子进程当然可以并发执行。

    B. 父进程和子进程共享一部分资源,但不能共享虚拟地址空间,在创建子进程时,会为子进程分配资源,如虚拟地址空间等。

    C. 进程控制块(PCB)是进程存在的唯一标志,每个进程都有自己的PCB。

    D. 临界资源一次只能被一个进程使用,正确

    ✅正确答案:B


    🍊2.操作系统创建新进程的过程

    Step1为新进程分配一个唯一的进程标识号,并申请一个空白PCB(PCB是有限的),若PCB申请失败,则创建失败。 

    Step2为进程分配所需资源,如文件、内存、I/O设备和CPU时间等。这些资源从操作系统获得,或从其父进程获得。如果资源不足(如内存),则此时并不是创建失败,而是处于创建态,等待内存资源。

    Step3初始化PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等。

    Step4若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。


    🍊3.引起进程创建的事件有哪些呢?

    1.用户登陆:分时系统中,用户登陆成功,系统会为其建立一个新的进程。

    2.作业调度:多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程

    3.提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求

    4.应用请求:由用户进程主动请求创建一个子进程

    📜习题检测


    📜086.题目难度 ⭐️⭐️

    086.进程创建时,不需要做的是( )。
    A.填写一个该进程的进程表项
    B.分配该进程适当的内存
    C.将该进程插入就绪队列
    D.为该进程分配CPU

    🍊详细题解:

    A. 是进程创建第一步做法。

    B. 是进程创建中step2中的做法:为进程分配所需资源。

    C. 是进程创建中step4的做法。

    D. 这是调度程序的工作,进程创建不包含此过程。

    ✅正确答案:D


    📜087.题目难度 ⭐️⭐️

    087.【2010统考真题】下列选项中,导致创建新进程的操作是( )。
    I.用户登录成功            II.设备分配            III.启动程序执行
    A.仅Ⅰ和ⅡI          B.仅ⅡI和III         C.仅I和III        D. I、II、III

    🍊详细题解:

    我们刚刚才讲过,用户登陆成功、启动程序执行都会创建一个新进程

    ✅正确答案:C


    📜088.题目难度 ⭐️⭐️

    088.【2021统考真题】下列操作中,操作系统在创建新进程时,必须完成的是( )。
    I.申请空白的进程控制块
    II.初始化进程控制块
    III.设置进程状态为执行态
    A.仅I           B.仅I、II              C.仅I、III           D.仅II、III

    🍊详细题解:

    本题考查进程创建时的细节过程,创建新进程时,一共会经历以下四个步骤:

    1.申请空白的PCB

    2.为新进程配置所需资源

    3.初始化PCB

    4.将PCB插入就绪队列(创建态——>就绪态)

    可见本题中只有I、II满足步骤

    ✅正确答案:B

     ✨✨✨我是分割线✨✨✨

    🥝8.3 进程控制之进程终止


    🍊1.操作系统终止进程的过程

    Step1:根据被终止进程的标识符(PID),检索出该进程的PCB,从中读出该进程的状态。

    Step2:若被终止的进程正处于运行态,应立即终止该进程的运行,将处理机资源分配给其他进程。

    Step3:若该进程还有子孙进程,则应当将其所有子孙进程终止。

    Step4:将该进程所拥有的全部资源,或归还给其父进程,或归还给操作系统。

    Step5:该PCB从所在队列中删除。


    🍊2.引起进程终止的事件有哪些呢?

    1.正常结束:表示进程的任务已完成并准备退出运行。

    2.异常结束:表示进程在运行时,发生了某种异常事件,使程序无法继续运行,如存储区越界、保护错、非法指令、特权指令错、运行超时、算术运算错、I/O故障等。

    3.外界干预:指进程应外界的请求而终止运行,如操作员或操作系统干预、父进程请求和父进程终止。

    ✨✨✨我是分割线✨✨✨

    🥝8.4 进程唤醒与阻塞


    🍊1.什么是进程的阻塞?

    正在执行的进程,由于期待某件事情未发生,如请求系统资源失败、等待某种操作完成、新数据尚未到达或无新任务可做等,进程便通过调用阻塞原语(Block),使自己由运行态变为阻塞态,可见,阻塞是进程自身的一种主动行为,因此只有处于运行态的进程(获得CPU),才可能将其转换为阻塞态。


    🍊2.进程是如何阻塞的呢?

    Step1:找到将要被阻塞的进程所对应的PCB。

    Step2:若该进程为运行态,则保护其现场,将其状态转为阻塞态,停止运行。

    Step3:把该PCB插入相应事件的等待队列,将处理机资源调度给其他就绪进程。


    🍊3.如何保护step2中的现场呢?

    保护现场,其实就是保护程序的上下文,当进程处于执行态时,处理机的许多信息都在寄存器中,当进程从运行态切换为其他状态时,通过将处理机状态信息保存在相应的PCB中,保护现场,以便该进程重新执行时,能从断点继续执行


    🍊4.什么是进程的唤醒?

    当被阻塞进程所期待的事件出现时,如它所期待的I/O操作已经完成,或期待的数据已经到达,由相关进程(比如:释放该I/O设备的进程,或提供数据的进程)调用唤醒原语(Wakeup),将等待该事件的进程唤醒,可见唤醒是一种被动行为。


    🍊5.进程是如何被唤醒的呢?

    Step1:在事件的等待队列中找到相应的PCB。

    Step2:将其从等待队列中移出,并置其状态为就绪态。

    Step3:把该PCB插入就绪队列,等待调度程序调度。


    ⚠️注意:Block原语和Wakeup原语是一对作用刚好相反的原语,必须成对使用。如果在某进程中调用了Block原语,就必须在与之合作的相关进程中安排一条相应的Wakeup原语,以便唤醒阻塞进程;否则,阻塞进程将会因不能被唤醒而一直处于阻塞状态!

    📜习题检测


    📜089.题目难度 ⭐️⭐️

    089.一个进程被唤醒,意味着( )。
    A.该进程可以重新竞争CPU
    B.优先级变大
    C.PCB移动到就绪队列之首
    D.进程变为运行态

    🍊详细题解:

    A. 进程被唤醒表示其期待的事件已经得到了满足,由阻塞态转化为就绪态,此时位于就绪态的进程将有机会重新竞争CPU资源。

    B. 进程被唤醒后的优先级是由固定算法来计算的,算法不同结果不同。

    C. 就绪队列是按照一定规则排序的,例如:先来先服务,或高优先级优先,或短进程优先。

    D. 进程不能一被唤醒就占有处理机运行,即要先经过就绪态,再转化为运行态。

    ✅正确答案:A


    📜090.题目难度 ⭐️⭐️

    090.【2019统考真题】下列选项中,可能会将进程唤醒的事件是( )。
    I.IO结束
    II.某进程退出临界区
    III.当前进程的时间片用完
    A.仅I              B.仅III              C.仅I、II              D.I、II、III

    🍊详细题解:

    进程被唤醒说明其期待的事件已经得到了满足:

    I.  操作系统期待的IO操作已经结束,即期待满足,等待该IO操作结束而被阻塞的相关进程会被唤醒。

    II. 操作系统期待访问的临界区此时有进程退出,操作系统可以使用临界资源,期待满足,之前因需要进入该临界区而被阻塞的相关进程会被唤醒

    III. 某进程时间片用完,此时该进程会由运行态切换为就绪态,等待重新调度。

    ✅正确答案:C

    🌕写在最后


    计算机操作系统世界是相当丰富的,内容方向繁多,但只要一步一个脚印,跟随【闪耀计划】吃透、搞懂、拿捏住操作系统内容是完全没有问题的!后期该系列还会有视频教程和经验分享,关于更多这方面的内容,请关注本专栏哦!

    热爱所热爱的, 学习伴随终生,kikokingzz与你同在!❥(^_-)

    展开全文
  • Linux进程控制(精讲)

    万次阅读 多人点赞 2021-09-23 21:55:48
    文章目录进程创建fork函数初始fork函数返回值写时拷贝fork常规用法fork调用失败的原因进程终止进程退出场景进程常见退出方法_exit函数exit函数return退出进程等待进程等待的必要性进程等待的方法wait方法waitpid方法...

    一、进程创建

    fork函数初识

    在Linux中,fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

    返回值:
    在子进程中返回0,父进程中返回子进程的PID,子进程创建失败返回-1。

    进程调用fork,当控制转移到内核中的fork代码后,内核做:

    • 分配新的内存块和内核数据结构给子进程。
    • 将父进程部分数据结构内容拷贝至子进程。
    • 添加子进程到系统进程列表当中。
    • fork返回,开始调度器调度。

    fork之后,父子进程代码共享。例如:
    在这里插入图片描述
    运行结果如下:
    在这里插入图片描述
    这里可以看到,Before只输出了一次,而After输出了两次。其中,Before是由父进程打印的,而调用fork函数之后打印的两个After,则分别由父进程和子进程两个进程执行。也就是说,fork之前父进程独立执行,而fork之后父子两个执行流分别执行。

    注意: fork之后,父进程和子进程谁先执行完全由调度器决定。

    fork函数返回值

    fork函数为什么要给子进程返回0,给父进程返回子进程的PID?

    一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。

    为什么fork函数有两个返回值?

    父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的进程地址空间、创建子进程对应的页表等等。子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了。
    在这里插入图片描述
    也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因。

    写时拷贝

    当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
    在这里插入图片描述
    这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术。

    1、为什么数据要进行写时拷贝?

    进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

    2、为什么不在创建子进程的时候就进行数据的拷贝?

    子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。

    3、代码会不会进行写时拷贝?

    90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

    fork常规用法

    1. 一个进程希望复制自己,使子进程同时执行不同的代码段。例如父进程等待客户端请求,生成子进程来处理请求。
    2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

    fork调用失败的原因

    fork函数创建子进程也可能会失败,有以下两种情况:

    1. 系统中有太多的进程,内存空间不足,子进程创建失败。
    2. 实际用户的进程数超过了限制,子进程创建失败。

    二、进程终止

    进程退出场景

    进程退出只有三种情况:

    1. 代码运行完毕,结果正确。
    2. 代码运行完毕,结果不正确。
    3. 代码异常终止(进程崩溃)。

    进程退出码

    我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。

    既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。

    当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。
    例如,对于下面这个简单的代码:
    在这里插入图片描述
    代码运行结束后,我们可以查看该进程的进程退出码。

    [cl@VM-0-15-centos procTermination]$ echo $?
    

    在这里插入图片描述
    这时便可以确定main函数是顺利执行完毕了。

    为什么以0表示代码执行成功,以非0表示代码执行错误?

    因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。

    C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:
    在这里插入图片描述
    运行代码后我们就可以看到各个错误码所对应的错误信息:
    在这里插入图片描述
    实际上Linux中的ls、pwd等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。
    可以看到,这些命令成功执行后,其退出码也是0。
    在这里插入图片描述
    但是命令执行错误后,其退出码就是非0的数字,该数字具体代表某一错误信息。
    在这里插入图片描述
    注意: 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

    进程正常退出

    return退出

    在main函数中使用return退出进程是我们常用的方法。

    例如,在main函数最后使用return退出进程。
    在这里插入图片描述
    运行结果:
    在这里插入图片描述

    exit函数

    使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:

    1. 执行用户通过atexit或on_exit定义的清理函数。
    2. 关闭所有打开的流,所有的缓存数据均被写入。
    3. 调用_exit函数终止进程。

    例如,以下代码中exit终止进程前会将缓冲区当中的数据输出。
    在这里插入图片描述
    运行结果:
    在这里插入图片描述

    _exit函数

    使用_exit函数退出进程的方法我们并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。

    例如,以下代码中使用_exit终止进程,则缓冲区当中的数据将不会被输出。
    在这里插入图片描述
    运行结果:
    在这里插入图片描述

    return、exit和_exit之间的区别与联系

    return、exit和_exit之间的区别

    只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

    使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。
    在这里插入图片描述

    return、exit和_exit之间的联系

    执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。
    在这里插入图片描述
    使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。

    进程异常退出

    情况一:向进程发生信号导致进程异常退出。

    例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。

    情况二:代码错误导致进程运行时异常退出。

    例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。

    三、进程等待

    进程等待的必要性

    1. 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
    2. 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
    3. 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
    4. 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。

    获取子进程status

    下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充。
    如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。

    status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只研究status低16比特位):
    在这里插入图片描述
    在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。
    在这里插入图片描述
    我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。

    exitCode = (status >> 8) & 0xFF; //退出码
    exitSignal = status & 0x7F;      //退出信号
    

    对于此,系统当中提供了两个宏来获取退出码和退出信号。

    • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
    • WEXITSTATUS(status):用于获取进程的退出码。
    exitNormal = WIFEXITED(status);  //是否正常退出
    exitCode = WEXITSTATUS(status);  //获取退出码
    

    需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。

    进程等待的方法

    wait方法

    函数原型:pid_t wait(int* status);

    作用:等待任意子进程。

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

    参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。

    例如,创建子进程后,父进程可使用wait函数一直等待子进程,直到子进程退出后读取子进程的退出信息。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    int main()
    {
    	pid_t id = fork();
    	if(id == 0){
    		//child
    		int count = 10;
    		while(count--){
    			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
    			sleep(1);
    		}
    		exit(0);
    	}
    	//father
    	int status = 0;
    	pid_t ret = wait(&status);
    	if(ret > 0){
    		//wait success
    		printf("wait child success...\n");
    		if(WIFEXITED(status)){
    			//exit normal
    			printf("exit code:%d\n", WEXITSTATUS(status));
    		}
    	}
    	sleep(3);
    	return 0;
    }
    

    我们可以使用以下监控脚本对进程进行实时监控:

    [cl@VM-0-15-centos procWait]$ while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
    

    这时我们可以看到,当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。
    在这里插入图片描述

    waitpid方法

    函数原型:pid_t waitpid(pid_t pid, int* status, int options);

    作用:等待指定子进程或任意子进程。

    返回值:
    1、等待成功返回被等待进程的pid。
    2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
    3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。

    参数:
    1、pid:待等待子进程的pid,若设置为-1,则等待任意子进程。
    2、status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
    3、options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。

    例如,创建子进程后,父进程可使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    int main()
    {
    	pid_t id = fork();
    	if (id == 0){
    		//child          
    		int count = 10;
    		while (count--){
    			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
    			sleep(1);
    		}
    		exit(0);
    	}
    	//father           
    	int status = 0;
    	pid_t ret = waitpid(id, &status, 0);
    	if (ret >= 0){
    		//wait success                    
    		printf("wait child success...\n");
    		if (WIFEXITED(status)){
    			//exit normal                                 
    			printf("exit code:%d\n", WEXITSTATUS(status));
    		}
    		else{
    			//signal killed                              
    			printf("killed by siganl %d\n", status & 0x7F);
    		}
    	}
    	sleep(3);
    	return 0;
    }
    

    在父进程运行过程中,我们可以尝试使用kill -9命令将子进程杀死,这时父进程也能等待子进程成功。
    在这里插入图片描述
    注意: 被信号杀死而退出的进程,其退出码将没有意义。

    多进程创建以及等待的代码模型

    上面演示的都是父进程创建以及等待一个子进程的例子,实际上我们还可以同时创建多个子进程,然后让父进程依次等待子进程退出,这叫做多进程创建以及等待的代码模型。

    例如,以下代码中同时创建了10个子进程,同时将子进程的pid放入到ids数组当中,并将这10个子进程退出时的退出码设置为该子进程pid在数组ids中的下标,之后父进程再使用waitpid函数指定等待这10个子进程。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    int main()
    {
    	pid_t ids[10];
    	for (int i = 0; i < 10; i++){
    		pid_t id = fork();
    		if (id == 0){
    			//child
    			printf("child process created successfully...PID:%d\n", getpid());
    			sleep(3);
    			exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标
    		}
    		//father
    		ids[i] = id;
    	}
    	for (int i = 0; i < 10; i++){
    		int status = 0;
    		pid_t ret = waitpid(ids[i], &status, 0);
    		if (ret >= 0){
    			//wait child success
    			printf("wiat child success..PID:%d\n", ids[i]);
    			if (WIFEXITED(status)){
    				//exit normal
    				printf("exit code:%d\n", WEXITSTATUS(status));
    			}
    			else{
    				//signal killed
    				printf("killed by signal %d\n", status & 0x7F);
    			}
    		}
    	}
    	return 0;
    }
    

    运行代码,这时我们便可以看到父进程同时创建多个子进程,当子进程退出后,父进程再依次读取这些子进程的退出信息。
    在这里插入图片描述

    基于非阻塞接口的轮询检测方案

    上述所给例子中,当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。

    实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。

    做法很简单,向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。

    例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    int main()
    {
    	pid_t id = fork();
    	if (id == 0){
    		//child
    		int count = 3;
    		while (count--){
    			printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
    			sleep(3);
    		}
    		exit(0);
    	}
    	//father
    	while (1){
    		int status = 0;
    		pid_t ret = waitpid(id, &status, WNOHANG);
    		if (ret > 0){
    			printf("wait child success...\n");
    			printf("exit code:%d\n", WEXITSTATUS(status));
    			break;
    		}
    		else if (ret == 0){
    			printf("father do other things...\n");
    			sleep(1);
    		}
    		else{
    			printf("waitpid error...\n");
    			break;
    		}
    	}
    	return 0;
    }
    

    运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。
    在这里插入图片描述

    四、进程程序替换

    替换原理

    用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。

    当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
    在这里插入图片描述

    当进行进程程序替换时,有没有创建新的进程?

    进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。

    子进程进行进程程序替换后,会影响父进程的代码和数据吗?

    子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

    替换函数

    替换函数有六种以exec开头的函数,它们统称为exec函数:

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

    第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

    例如,要执行的是ls程序。

    execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
    

    二、int execlp(const char *file, const char *arg, ...);

    第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

    例如,要执行的是ls程序。

    execlp("ls", "ls", "-a", "-i", "-l", NULL);
    

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

    第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。

    例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

    char* myenvp[] = { "MYVAL=2021", NULL };
    execle("./mycmd", "mycmd", NULL, myenvp);
    

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

    第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。

    例如,要执行的是ls程序。

    char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
    execv("/usr/bin/ls", myargv);
    

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

    第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。

    例如,要执行的是ls程序。

    char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
    execvp("ls", myargv);
    

    六、int execve(const char *path, char *const argv[], char *const envp[]);

    第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。

    例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

    char* myargv[] = { "mycmd", NULL };
    char* myenvp[] = { "MYVAL=2021", NULL };
    execve("./mycmd", myargv, myenvp);
    

    函数解释

    • 这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
    • 如果调用出错,则返回-1。

    也就是说,exec系列函数只要返回了,就意味着调用失败。

    命名理解

    这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:

    • l(list):表示参数采用列表的形式,一一列出。
    • v(vector):表示参数采用数组的形式。
    • p(path):表示能自动搜索环境变量PATH,进行程序查找。
    • e(env):表示可以传入自己设置的环境变量。
    函数名参数格式是否带路径是否使用当前环境变量
    execl列表
    execlp列表
    execle列表否,需自己组装环境变量
    execv数组
    execvp数组
    execve数组否,需自己组装环境变量

    事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。

    下图为exec系列函数族之间的关系:
    在这里插入图片描述

    做一个简易的shell

    shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可。
    在这里插入图片描述
    其实shell需要执行的逻辑非常简单,其只需循环执行以下步骤:

    1. 获取命令行。
    2. 解析命令行。
    3. 创建子进程。
    4. 替换子进程。
    5. 等待子进程退出。

    其中,创建子进程使用fork函数,替换子进程使用exec系列函数,等待子进程使用wait或者waitpid函数。

    于是我们可以很容易实现一个简易的shell,代码如下:

    #include <stdio.h>
    #include <pwd.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #define LEN 1024 //命令最大长度
    #define NUM 32 //命令拆分后的最大个数
    int main()
    {
    	char cmd[LEN]; //存储命令
    	char* myargv[NUM]; //存储命令拆分后的结果
    	char hostname[32]; //主机名
    	char pwd[128]; //当前目录
    	while (1){
    		//获取命令提示信息
    		struct passwd* pass = getpwuid(getuid());
    		gethostname(hostname, sizeof(hostname)-1);
    		getcwd(pwd, sizeof(pwd)-1);
    		int len = strlen(pwd);
    		char* p = pwd + len - 1;
    		while (*p != '/'){
    			p--;
    		}
    		p++;
    		//打印命令提示信息
    		printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
    		//读取命令
    		fgets(cmd, LEN, stdin);
    		cmd[strlen(cmd) - 1] = '\0';
    		//拆分命令
    		myargv[0] = strtok(cmd, " ");
    		int i = 1;
    		while (myargv[i] = strtok(NULL, " ")){
    			i++;
    		}
    		pid_t id = fork(); //创建子进程执行命令
    		if (id == 0){
    			//child
    			execvp(myargv[0], myargv); //child进行程序替换
    			exit(1); //替换失败的退出码设置为1
    		}
    		//shell
    		int status = 0;
    		pid_t ret = waitpid(id, &status, 0); //shell等待child退出
    		if (ret > 0){
    			printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码
    		}
    	}
    	return 0;
    }
    

    效果展示:

    在这里插入图片描述
    说明:
    当执行./myshell命令后,便是我们自己实现的shell在进行命令行解释,我们自己实现的shell在子进程退出后都打印了子进程的退出码,我们可以根据这一点来区分我们当前使用的是Linux操作系统的shell还是我们自己实现的shell。

    展开全文
  • Linux进程管理

    千次阅读 多人点赞 2022-03-19 14:07:43
    关于进程,环境变量和程序地址空间的学习

    冯诺依曼体系结构

    我们熟悉的计算机——笔记本,不熟悉的计算机——服务器,他们都是在冯诺依曼体系结构的基础上,底层搭载不同的硬件结构,上层由操作系统管理的。冯诺依曼体系结构如下图所示:
    在这里插入图片描述

    • 输入设备:包括键盘、鼠标、磁盘、网卡、摄像头、话筒等
    • 存储器:内存
    • 中央处理器:包含运算器和控制器
    • 输出设备:显示器、磁盘、网卡、音响等

    在这里我们需要注意以下几点:

    • 存储器所指的是内存,并非磁盘,硬盘等存储器件。
    • CPU只能与内存进行读写,不能访问输入输出设备
    • 输入输出设备读写数据也只能与内存进行交互

    总结:内存是体系结构的核心设备,CPU与外设之间的信息交互都需要依靠内存!!

    操作系统

    什么是操作系统呢?

    操作系统就是一款专门针对软硬件资源进行管理工作的软件

    为什么需要操作系统呢?

    对下:与硬件交互,管理所有的软硬件资源
    对上:为用户程序(应用程序)提供一个良好的执行环境

    操作系统如何管理?

    先描述:用struct结构体描述对象
    再组织:用链表或者其他搞笑的数据结构进行组织

    系统调用与库函数

    系统调用是操作系统对外提供的一些接口,供上层开发使用。
    库函数是存放在函数库中的函数。
    那么系统调用和库函数有什么关系呢?

    系统调用和库函数是上下层关系,库函数是用户对系统调用的进一步封装,库函数对硬件进行操作时,会调用系统提供的API。

    进程

    通俗来讲,进程就是一个正在执行的程序。在这里我们理解为进程进程控制块PCB数据代码构成。当然进程不止这几部分,还有进程地址空间和页表等。

    描述进程

    操作系统中同时存在许多进程,每个进程各不相同,操作系统如何管理不同的进程呢?
    用操作系统的六字真言“先描述,再组织”,描述进程用到进程控制块PCB,在Linux操作系统下的PCB称为:task_struct
    task_struct中存储的进程的信息,其主要可以分为以下几类:

    • 标识符:也叫做PID,描述本进程的唯一标识符,用来区别其他进程
    • 状态:任务状态、退出代码、退出信号等
    • 优先级:相对于其他进程的优先级
    • 程序计数器:PC指针,用于保存程序下一条执行指令的地址
    • 内存指针:包括程序代码和进程相关数据的指针,通过内存指针可以找到程序文件
    • 上下文数据:进制执行时CPU的寄存器中数据
    • I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表等
    • 记账信息:包括处理器时间总和,使用的时钟数总和、时间限制、记账号等
    • 其他信息

    上下文数据非常重要,因为在进程切换时,寄存器中的数据会被保存在PCB中,为了下次切换回来时,CPU可以找到上次进程运行的地方。通过上下文数据,我们才可以感受到进程是被切换的。

    查看进程

    查看进程的方法有两种:
    首先我们写一个死循环的程序,让程序一直运行着,并输出该进程的PID和PPID。

    #include <iostream>
    #include <unistd.h>
    #include <cstdlib>
    using namespace std;
    int main()
    {
    	while(1)
    	{	
    		std::cout << "pid =" << getpid() << " ppid =" << getppid() << std::endl;
       		sleep(1);
    	}
    	return 0;
    }  
    
    • 通过/proc系统文件查看

    在这里插入图片描述

    • 通过ps - axj | grep 文件名

    在这里插入图片描述

    创建进程

    我们通过系统调用fork函数创建进程

    #include <iostream>
    #include <unistd.h>
    #include <cstdlib>
    int main()
    {                    
    	// fork的验证 
    	int ret = fork();
    	std::cout << "ret =" << ret << ",proc =" << getpid() << " parent =" << getppid() << std::endl; 
    	sleep(1); 
    	return 0;
    }
    

    运行该程序,我们可以得到以下结果:
    在这里插入图片描述
    从以上结果我们可以得到以下结论:

    • 创建子进程后,会有两个返回值,父进程的返回值是子进程的PID,子进程的返回值为0
    • 父进程和子进程的代码共享

    但通常情况下,fork创建子进程后,我们需要通过返回值进行分流操作,目的是为了子进程与父进程做不一样的事情。

    #include <iostream>
    #include <unistd.h>
    #include <cstdlib>
    int main()
    {    
    	cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << endl;
    	pid_t ret = fork();       
    	if(ret == 0)              
    	{                         
     		while(1)                
    		{                       
    			cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
    			sleep(1);             
    		}                       
    	}                         
    	else if(ret > 0)          
    	{                         
    		while(1)                
    		{                       
    			cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
    			sleep(2);             
    		}                       
    	}                         
    	else                      
    	{                         
    		cout << "fork failed" << endl;  
    	}
    	sleep(1); 
     	return 0;
    }
    

    执行上述程序后的结果为:
    在这里插入图片描述
    从以上结果我们可以得到以下结论:

    • 由于PC指针的存在,fork创建的子进程并不执行fork语句前的代码

    通过以上两段程序,我们该如何理解fork创建进程呢?
    fork创建进程表示系统中多了一个进程,而进程与进程相关的内核数据结构进程的数据和代码组成。
    那么子进程的内核数据结构、数据和代码从何而来呢?

    子进程的task_struct:会以父进程为模板,初始化子进程的task_struct
    子进程的代码:和父进程共享一份代码,因为程序运行的时间,代码无法被修改
    子进程的程序:默认情况下,子进程和父进程数据共享,但是当数据发现改变的时间,会“写时拷贝

    进程状态

    进程控制块中有一个叫进程状态的信息,进程状态标志着此进程当前的运行情况。一个进程可以有以下几种状态:

    • R 运行状态:进程不一定在运行,表明进程要么在运行中,要么在运行队列里。
    • S 睡眠状态:,可中断睡眠,进程在等待事件完成,此时进程处于等待队列。
    • D 磁盘休眠状态:不可中断睡眠状态,在这个状态的进程通常会等待IO的结束。
    • T 停止状态:发送 SIGSTOP 信号停止进程。发送 SIGCONT 信号让进程继续运行。
    • X 死亡状态:这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

    在这里插入图片描述
    Z-僵尸进程

    • 僵尸状态是一个比较特殊的状态,当进程退出并且父进程没用读取到子进程的退出码时,就会产生僵尸状态。
    • 僵尸进程会以终止状态保持在进程表中,并且一直等待父进程读取退出状态代码
    #include <iostream>
    #include <unistd.h>
    #include <cstdlib>
    int main()
    {  
    	pid_t ret = fork();                                                                                                           
      	if(ret > 0)
      	{
        	while(1)
        	{
          		cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
          		sleep(2);
        	}
      	}
      	else if(ret == 0)
      	{
        	while(1)
        	{
          		cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
          		sleep(20);
          		exit(1);
        	}
      	}
      	else
     	{
        	exit(1);
      	}
        sleep(1);
    	return 0}
    

    运行结果如图所示:
    在这里插入图片描述
    僵尸进程如果一直不进行处理,PCB需要一直维护,占用系统的空间;同时一个父进程创建了许多子进程,如果不回收就会造成内存资源的泄漏
    孤儿进程

    • 父进程如果提前退出,子进程就会成为孤儿进程,孤儿进程会被1号init进程领养。
    #include <iostream>
    #include <unistd.h>
    #include <cstdlib>
    int main()
    {
    	pid_t ret = fork();                                                                                                           
      	if(ret > 0)      
      	{      
          	cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;      
          	sleep(10);      
          	exit(1);      
      	}      
      	else if(ret == 0)      
      	{      
        	while(1)      
        	{      
          		cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;      
          		sleep(1);      
        	}      
      	}      
      	else      
      	{      
        	exit(1);      
      	}      
      	sleep(1);      
      	return 0;
    }
    

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

    进程优先级

    进程优先级表示CPU资源分为的先后顺序,也就是指进程的优先权。
    如何查看进程的优先级呢?

    • ps -l

    在这里插入图片描述

    • UID:代表执行者的身份
    • PID:该进程的代号
    • PPID:该进程的父进程的代号
    • PRI:代表这个进程可被执行的优先级,其值越小越好
    • NI:代表这个进程的NICE值

    进程的PRI默认值都是80,用户通过调整NICE值修改进程的优先级,而NICE的取值范围为(-20~19)

    NI的取值范围较小原因:优先级再怎么设置,也只能是一个相对的优先级,不能出现绝对的优先级,否则会出现“饥饿问题”

    top指令修改已存在进程的NICE值。

    top -> 按r -> 输入进程的PID -> 输入nice值

    环境变量

    环境变量一般是指操作系统中用来指定操作系统运行环境的一些参数。
    环境变量通常具有某些特殊用途,在系统中通常具有全局特性
    常见的环境变量:

    • PATH:指定命令的搜索路径
    • HOME:指定用户的主工作目录
    • SHELL:当前Shell,通常是/bin/bash

    和环境变量相关的命令

    • echo:显示某个环境变量值
    • export:设置一个新的环境变量
    • env:显示所有环境变量
    • unset:清除环境表里
    • set:显示本地定义的Shell变量和环境变量

    查看环境变量的方法

    echo $Name

    在这里插入图片描述
    如何通过代码获得环境变量呢?

    1. 命令行第三个参数
    int main(int argc, char* argv[], char *env[])    
    {    
      for(int i =0; env[i]; ++i)    
      {    
        printf("%d -> %s\n", i, env[i]);    
      }    
      return 0;                                                                               
    } 
    
    1. 通过第三方变量environ获取
    #include <stdio.h>
    int main(int argc, char *argv[])
    {
    	extern char** environ;
    	int i = 0;
    	for(; environ[i]; i++){
    		printf("%s\n", environ[i]);
    	}
    	return 0;
    }
    
    1. 通过系统调用获取或设置环境变量
    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
    	printf("%s\n", getenv("PATH"));
    	printf("%s\n", getenv("HOME"));
    	printf("%s\n", getenv("SHELL"));
    	return 0;
    }
    

    环境变量通常是具有全局属性的
    我们通过设定了一个本地变量MYENV = “sherry”

    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
    	char * env = getenv("MYENV");
    	if(env)
    	{
    	printf("%s\n", env);
    	}
    	return 0;
    }
    

    首先在命令行设定一个本地变量MYENV,通过set | grep MYENV 显示出本地变量的值,但运行env | grep MYENV./myproc发现无法在环境变量中找到MYENV,说明此时MYENV只是一个本地变量,不是环境变量,通过export命令,将其设置为环境变量,再运行env | grep MYENV./myproc可以发现已经有输出结果。
    MYENV变为环境变量后,不仅仅可以通过命令输出,还可以通过程序输出,因此可以证明环境变量具有全局属性
    在这里插入图片描述

    程序地址空间

    在学习C语言的时候,我们经常看见下面这张图:
    在这里插入图片描述
    通过一段代码打印出不同区域的地址可以帮我们理解区域的划分:

    #include <stdio.h>                                                                    
    #include <unistd.h>               
    #include <string.h>                   
    #include <stdlib.h>
    int g_unval;    
    int g_val = 100;    
    int main()    
    {    
      const char* s = "sherry";    
      printf("code addr:%p\n", main);    
      printf("string rdonly addr:%p\n", s);    
      printf("int addr:%p\n", &g_val);    
      printf("unint adde:%p\n", &g_unval);    
        
      char* heap = (char*)malloc(10);    
      printf("heap addr:%p\n", heap);    
      printf("stack addr:%p\n", &s);    
          
      int a = 1;    
      int b = 1;    
      int c = 1;    
      printf("stack addr:%p\n", &a);    
      printf("stack addr:%p\n", &b);    
      printf("stack addr:%p\n", &c);    
    } 
    

    在这里插入图片描述
    那么这里的内存地址是不是我们经常说的物理内存呢?
    我们通过以下代码进行验证:

    #include <stdio.h>                                                                    
    #include <unistd.h>               
    #include <string.h>                   
    #include <stdlib.h>
    int main()    
    {    
      int g_val = 100;    
      int cnt = 5;                                                                            
      int ret = fork();    
      while(cnt)    
      {    
      	if(ret > 0)    
        {    
          printf("parent[%d]: %d: %p\n",getpid(), g_val, &g_val);    
        }    
        if(ret == 0)    
        {    
          if(cnt == 3)    
          {    
            g_val = 200;    
          }    
          printf("child[%d]: %d: %p\n",getpid(), g_val, &g_val);    
        }    
        cnt--;    
        sleep(1);    
      }    
      return 0;    
    }  
    

    通过运行结果可以发现,在子进程中修改了g_val的值,父进程的g_val不改变,因为“写时拷贝”的原因,但是我们惊奇的发现父进程和子进程的g_val的地址一模一样。
    结论:这里的地址绝对不是物理地址,而我们把他叫做虚拟地址
    在这里插入图片描述
    进程的虚拟地址和系统物理地址又有什么关联呢?
    进程的虚拟地址本质上是内核上的一中数据类型,可以用结构体来描述。

    struct mm_struct
    {
    	int code_strat;
    	int code_end;
    	
    	int init_data_strat;
    	int init_data_end;
    	
    	int uninit_datae_strat;
    	int uninit_datae_end;
    	
    	int heap_strat;
    	int heap_end;
    	
    	int head_strat;
    	int head_end;
    }
    

    虽然每个进程都一个自己的mm_struct,但是每个进程都认为他的mm_struct代表整个内存,且所有的地址为0x0000…000~0xFFFF…FFF。
    虚拟地址通过*_start和*_end的形式把自己划分成不同的区域,不同的区域有着自己的地址界限。
    通过页表和MMU建立虚拟地址物理地址映射关系。
    在这里插入图片描述

    OS为何要通过页表来实现虚拟地址和物理地址的映射关系呢?

    • 通过添加一层软件层,完成对进程的内存操作进行管理,本质是为了保护物理内存及各个进程的安全

    用户的误操作可能会越界访问不属于自己的地址空间,对其他地址上的内容进行修改等。使用虚拟内存,就有效保护了真实物理内存空间上的内容。

    • 将内存申请与内存使用在时间上划分清楚,通过虚拟地址空间来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件上的分离

    也许进程会开辟一块很大的空间,但是进程并不是立即使用,因此OS可以开辟虚拟空间,物理内存用于一些真正需要的地方,等到进程需要使用时,再为其开辟空间,这样有效的提高效率。

    • 站在CPU的角度,进程统一看作使用4GB,而每个空间区域的相对位置是比较确定的

    操作系统只为了达到一个目的:每一个进程都认为自己是独占系统资源的!

    通过以上的进程地址空间学习后,以后我们再描述进程,就不仅仅是PCB、数据和代码了。
    进程 = 进程控制块task_struct + 进程地址空间mm_struct + 页表 + 数据和代码

    展开全文
  • 进程和线程的区别(超详细)

    万次阅读 多人点赞 2019-10-03 21:57:46
    进程和线程 进程 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。 线程 进程中的一个执行任务(控制单元),负责...
  • 什么是僵尸进程与孤儿进程

    千次阅读 多人点赞 2021-10-12 03:52:47
    当一个进程调用 exit 命令结束自己的生命时,其实它并没有真正的被销毁,内核只是释放了该进程的所有资源,包括打开的文件、占用的内存等,但是留下一个称为僵尸进程的数据结构,这个结构保留了一定的信息(包括进程...
  • Linux C/C++编程之(十六)进程进程控制

    千次阅读 多人点赞 2020-07-07 11:47:43
    10)回收子进程 孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。 僵尸进程:子进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,...
  • 进程

    万次阅读 2021-05-25 14:29:00
    文章目录进程一.什么是多任务二、实现多任务的方式:示例1 一般情况下执行唱歌、跳舞示例2 单线程执行函数示例3 多线程执行函数示例4多线程执行唱歌、跳舞1示例5多线程执行唱歌、跳舞2三、什么是进程1.什么是进程?2...
  • Python 多进程进程间通信

    千次阅读 多人点赞 2020-09-03 21:32:33
    作者:billy 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明...进程与程序不同,程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。 创建进程的常用方式 在 Pyth
  • 线程与进程,你真得理解了吗

    万次阅读 多人点赞 2022-05-06 19:05:25
    相信大家面试时一定没少被一个问题刁难,那就是进程和线程的区别是什么?这个问题延申开来并不像表面那么简单,今天就来深入一探。 开始前先看一组非常传神的图例,相信可以帮助你更好理解进程与线程的概念: 1 ...
  • 【操作系统】第六话·线程是进程的(宝ᴗ宝)嘛?

    千次阅读 多人点赞 2022-03-12 15:23:07
    在同一进程中,线程的切换不会引起进程切换,但从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。 (2)并发性 引入线程的操作系统中,不仅进程之间可以并发执行,而且一个进程中的多个线程之间亦可...
  • Linux任务管理与守护进程

    千次阅读 多人点赞 2022-02-06 12:59:49
    文章目录任务管理进程组概念作业概念会话概念相关操作前台进程&后台进程jobs、fg、bgps命令查看指定的选项守护进程 任务管理 进程组概念 每个进程除了有一个进程ID之外,还属于一个进程组,进程组是一个或多个...
  • 进程间的六种通信方式

    千次阅读 多人点赞 2022-05-07 11:24:22
    每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的, 所以进程之间要通信必须通过内核。 进程间通信目的一般有共享数据,数据传输,消息通知,进程控制等。以 Unix/...
  • linux 如何清理僵尸进程

    千次阅读 2021-05-09 00:40:19
    今天在维护服务器的时候,发现有5个nova-novncproxy的僵尸进程。之前对于僵尸进程的了解并不深,赶紧找了篇相关文章来学习一下,该如何处理。定义In UNIX System terminology, a process that has terminated,but ...
  • 一文读懂Linux进程进程组、会话、僵尸

    千次阅读 多人点赞 2020-02-24 08:21:00
    而对于僵尸进程,如果进程不调用 wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能...
  • 进程管理

    万次阅读 多人点赞 2021-05-05 22:15:07
    进程管理
  • 【操作系统】_7种进程调度算法

    千次阅读 2022-04-26 23:43:41
    书中一共描述了七种进程调度算法,为了学到这几种调度算法,后边做了几道练习题。 1. 先来先服务(FCFS)调度算法 先来先服务调度算法是最简单的调度方法。其基本原则是,按照进程进入就绪队列的先后次序进行选择。...
  • Linux -- 多进程编程之 - 基础实现、孤儿进程

    千次阅读 多人点赞 2021-10-24 20:26:21
    在Linux 中创建一个新进程的方法是使用 fork()函数。
  • 本文基于CentOS,深入解释进程的优先级,环境变量,程序地址空间,进程地址空间,进程调度队列。 目录 一、进程优先级 1.基本概念 2.查看系统进程 3. PRI && NI 4.其他概念 二、环境变量 1. 基本...
  • 获取进程编号 ** 学习目标 ** 能够知道如果获取进程编号 1. 获取进程编号的目的 获取进程编号的目的是验证主进程和子进程的关系,可以得知子进程是由那个主进程创建出来的。 获取进程编号的两种操作 获取当前...
  • Linux系统管理----centos7系统进程管理

    万次阅读 2022-03-06 19:22:27
    一、进程概念 1.进程组成部分: 2.僵尸进程: 3.线程: 4.上下文切换: 5.中断处理: 6.进程的属性 二、ps查看进程工具 1.-aux弹出消息及详解 2、linux进程有5种基本状态 三、free 查看内存使用情况 1.弹...
  • Linux 进程和计划任务管理

    万次阅读 2021-08-17 09:39:34
    进程和内存管理2.1 什么是进程 1. 前言 2. 进程和内存管理 2.1 什么是进程 Process: 运行中的程序的一个副本,是被载入内存的一个指令集合,是资源分配的单位 进程 ID(Process ID,PID)号码被用来标记各个进程 ...
  • Linux常用命令(十)查看和控制进程

    千次阅读 2021-05-09 01:28:18
    Linux常用命令(十)查看和控制进程一、查看进程1、ps命令——查看静态的进程统计信息(Processes Statistic)ps命令时Linux系统中最为常用的进程查看工具,主要用于显示包含当前运行的各进程完整信息的静态快照。...
  • 什么是进程同步 进程互斥的原则 进程互斥的软件实现方法 1、单标志法 2、双标志先检查法 3、双标志后检查法 4、Peterson 算法 进程互斥的硬件实现方法 1、中断屏蔽方法 2、TestAndSetLock 指令 TSL和中断屏蔽的区别 ...
  • 易语言通过进程名称获取进程ID

    千次阅读 2020-06-05 09:33:03
    通过系统映像的名称来获取进程的PID,有了PID就可以通过(进程_ID取窗口句柄) 获取窗口句柄,有了句柄就可以大漠后台绑定窗口,所以最终目的还是通过进程名称来获取进程所在窗口的句柄,用来窗口的绑定操作。...
  • Android应用程序进程管理

    千次下载 热门讨论 2013-10-23 01:21:26
    Android系统里面的应用程序进程有一个特点,那就是它们是被系统托管的。也就是说,系统根据需要来创建进程以及回收进程进程创建发生在组件启动时,它们是由Zygote进程负责创建。Zygote进程是由系统中的第一个进程...
  • Python多进程

    万次阅读 多人点赞 2021-09-30 19:50:24
    进程示例 举例一个吃饭活动,有一碗汤和一碗饭,正常操作是一口饭一口汤(当然你先把汤或者饭一口气吃完,当我无话可说)。单进程只能是先吃完饭,再喝汤;或者是先喝完汤,再吃饭。 看代码 import time def ...
  • 信号是 Linux 系统响应某些条件而产生的一个事件,由操作系统事先定义,接收到该信号的进程可以采取自定义的行为,这是一种“订阅-发布”的模式。 信号来源分为硬件来源和软件来源: 硬件来源:如按下 CTRL+C、除 ...
  • 进程管理】

    千次阅读 2021-12-24 10:50:11
    程序和进程的区别进程的生命周期监控进程状态静态查看进程ps每列含义详解STAT状态含义进程状态切换-范例1不可中断进程-范例2僵尸进程-范例3动态查看进程-top每列含义详解如何理解中断什么是中断?为什么要有中断呢?...
  • 终于把进程和线程学会了

    万次阅读 多人点赞 2020-07-24 23:59:36
    主要有:线程和进程的讲解、Java实战多线程、进程和线程的面试题总结等内容

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,165,466
精华内容 1,266,186
关键字:

进程