• Linux —— 进程概念超详解!(持续更新……)


    目录

    1.什么是进程?

    2.进程的状态

    3.Linux是怎么做的

    4.Linux的进程管理

    5.僵尸进程

    6.孤儿进程

    7.进程优先级

     8.进程的四个重要概念

    9.环境变量

    1.什么是进程?

    即使我们不会编程,我们依然可以很容易的理解什么是进程。如果我们是windos操作系统,我们可以打开任务管理器,点击进程一行可以观察进程:

     我们可以发现,进程占用了cpu与内存的空间,并且我们知道:cpu有强大的指令集,可以处理很多很多指令,但是cpu只能被动的接收别人的指令,然后处理别人的数据。这些指令和数据从何而来?事实上在我们双击运行一个可执行文件时,这个可执行文件就会被加载到内存当中去,此时,内存当中被加载的可执行文件就是进程。此时在操作系统的角度看来,二进制指令冗余并且繁杂,非常难以管理,所以操作系统会生成一个内核结构体——PCB结构体。我们知道C语言中的结构体是用来描述某一时间的属性的,那么进程也是一个事件,进程也有属性一个进程对应一个PCB结构体,操作系统只需通过此结构体就能找到对应的二进制指令。那么当有多个程序被加载到内存时,就会生成多个PCB结构体,操作系统非常聪明,把这些结构体通过某种数据结构组织起来,这样操作系统可以非常方便的访问各个PCB结构体。

     我们可以用这么一句话来描述操作系统有多厉害:操作系统既管做饭,又管喂饭,还负责洗碗……刚才我们也说了,cpu只能被动的接收指令,这些指令就相当于饭,这些饭通过操作系统给cpu“喂”过去,然后cpu只需要负责“吃”。刚才也说了,操作系统是通过PCB找到原生代码的,那么给cpu送过去的也是PCB结构体,如果进程很多,那操作系统是等cpu执行完一个进程后再喂一个PCB给cpu吗?当然不是!cpu有一个运行队列,操作系统把PCB结构体放在cpu的运行队列上,当PCB结构体到cpu的门口了,操作系统才会通过PCB找到原生代码,再将这些代码喂给cpu。

    2.进程的状态

    有了PCB结构体和数据结构,很大程度上能够提升管理进程的效率,但远远不够。我们需要将进程进行分类,映射到现实生活当中也是如此:例如图书馆,图书管理员为了方便管理图书,会为每本书打上简介,并通过把书放在书架上的方式来存储图书……那么进程如何分类?通过状态。每个进程都有自己的状态,这些状态能够告诉操作系统我正在干什么、我将要干什么,也就是说,进程的多种状态,本质都是为了满足未来的某种使用场景

    如果我们熟悉windows,我们可以大体知道进程的一些状态:

     那么我们接下来着重介绍的便是三种状态:

    • 运行状态
    • 阻塞状态
    • 挂起状态

    运行状态:按照字面理解的话,那就是cpu正在处理的进程。这句话本没有错,但我认为运行状态是指:进程的PCB结构体正在cpu的运行队列上排队

    阻塞状态:每种硬件都有一个等待队列,那么进程的PCB结构体被操作系统放在这个等待队列时,这个进程就处于阻塞状态,通常也称等待状态。

    挂起状态:内存满负荷时,又要增加新的进程显然是不行的。所以操作系统会观察内存中的哪些进程没有被放在任何一个队列里面(在内存里面啥也不干),找到以后就把此进程的代码和数据短期内置换到磁盘上,仅保留此进程的PCB。腾出的这一块空间供新的进程使用。这个动作,就叫做挂起,被挂起的进程,状态处于挂起状态。

    实际上,不同的进程状态,其本质就是处于不同的队列

    3.Linux是怎么做的

    上面介绍的是非常枯燥的操作系统的理论知识,现在我们把目光聚集在Linux这款操作系统上,观察Linux是如何描述、管理进程的。

    那么在Linux内核里,是如何描述进程状态的呢?

    1. /*
    2. * The task state array is a strange "bitmap" of
    3. * reasons to sleep. Thus "running" is zero, and
    4. * you can test for combinations of others with
    5. * simple bit tests.
    6. */
    7. static const char * const task_state_array[] = {
    8. "R (running)", /* 0 */
    9. "S (sleeping)", /* 1 */
    10. "D (disk sleep)", /* 2 */
    11. "T (stopped)", /* 4 */
    12. "t (tracing stop)", /* 8 */
    13. "X (dead)", /* 16 */
    14. "Z (zombie)", /* 32 */
    15. };

    源码关于进程状态的描述是一个指针数组,其中各个字母的含义为:

    • R (Running):该进程正在运行中。
    • S (Sleep):该进程目前正在睡眠状态,但可以被唤醒。
    • D :不可被唤醒的睡眠状态,通常这个进程可能在等待I/O的情况。
    • T :停止状态,发送一个暂停信号给进程,进程就暂停了。
    • t :追踪停止状态,通常在断点调试时,进程处于此状态。
    • X :死亡状态,这个状态是用来告诉操作系统的,所以我们观察不到此状态。
    • Z (Zombie):僵尸状态,进程已经死亡,但是却无法被删除至内存外。

    那么想要在Linux环境下观察进程的状态,我们可用的指令有两个:

    ps aux 或者 ps ajx                <==查看系统所有的进程

    ps -lA                <==也是能够查看系统的所有进程

     我们截取一段截图,来观察Linux的具体描述:

    可以看见,指令的下一行就是列表参数,其中的STAT就是咱们要找的进程状态。此时我们的指令正处于运行状态(观察的进程状态是当时发生的动作,所以是运行状态)。

    我们也应该注意到PID。PID是什么?PID是进程的唯一标识符,这个标识符是操作系统给的,操作系统把PID放在进程的PCB中。

    现在,我们学会了最基本的查看进程。此时我们需要结合我们自己写的程序来进一步熟悉进程和Linux。

    我们输入以下代码并比编译生成一个名为test的可执行文件:

    1. 1 #include
    2. 2 int main()
    3. 3 {
    4. 4 while(1);
    5. 5 return 0;
    6. 6 }

    然后运行此程序,观察此进程的状态。为了提高效率,我们的指令可以升级为:

    ps ajx | head -1 && ps ajx | grep test

    可以看到,我们自己的程序处于运行状态啦!并且拥有自己的PID。但是我们看到,还有一个叫做PPID的东西,这个PPID是我们的进程创建时所依赖的进程的标识符。 我们再通过一条指令去观察这个PPID是谁的标识符:

    ps ajx | head -1 && ps ajx | grep 4034                <==这个PPID不唯一

     

    可以看到,这个4034的PID是bash进程的标识符,这个bash进程也有一个PPID。bash是什么?我们可以理解为命令输入行。这个命令行输入也就是让我们输入指令的地方,是不是我们一打开Linux就有了?所以,bash是当我们登录Linux操作系统时,操作系统会自动获取一个bash的shell(与以前介绍的shell原理一样,我们并不能直接操作bash),那么此时bash就被加载到内存里面去啦!所以bash是登录操作系统时自动加载到内存里的进程。在这个bash进程的基础上,我们才可以输入指令去让Linux完成某些操作,所以,指令的运行是依赖bash的。那么这种依赖关系,我们称作[父子进程],指令需要依赖bash,所以指令称为[子进程],bash称为[父进程]。同理,我们在命令输入行输入[ls]这个指令,ls是子进程,bash是父进程。

    现在,我们需要知道Linux是如何描述阻塞状态的。我们复习一下阻塞状态的定义:进程的PCB被加载到硬件的等待队列当中。那么C语言有什么函数能够访问硬件?printf嘛!在原有的代码基础上修改程序:

    1. 1 #include
    2. 2 int main()
    3. 3 {
    4. 4 while(1)
    5. 5 {
    6. 6 printf("访问硬件啦!\n");
    7. 7 }
    8. 8 return 0;
    9. 9 }

    运行代码,再输入指令观察进程的状态: 

    这里绝对有人有疑问,我们明明运行程序了啊!为什么是S状态!而不是R状态!实际上这两种状态都有!我们要清楚一个概念,硬件的处理速度有cpu快吗?当然没有!cpu处理指令的单位时间是纳秒!所以可以打一个比方,执行一个程序要的时间为100秒,那么这个进程在硬件的等待队列当中就可能待了99.99秒,所以从概率上来看,我们的test进程是S状态,就理解啦!所以呢,Linux中,使用S状态描述阻塞状态,挂起也用S状态描述。绝对不是因为懒,Linus才这么设计的,而是因为任何一个操作系统的使用者,都不会关心这些问题。

    还有一个D状态,我在这里浅浅的普及一下:D状态是平时使用时几乎不会碰到的东西。D状态是一个深度睡眠的状态,通常发生在与硬件的交互中。比如,当我们写的代码,要访问硬件,要交换的数据又臭又长多到无边无际,再加上硬件的处理速度很慢,这就会导致其他的进程要访问这个硬件,必须在等待前一个进程与硬件交互完毕。但是这毕竟是个合法状态,我们没有办法使用kill指令杀掉这个进程,因为如果杀掉这个进程,硬件就非常尴尬了(怎么深入了解还没到一半人就没了?),在操作系统的角度来看,这样的交互是合法的,因为我们的进程设计就是要先与硬件交互再被cpu执行啊,有毛病吗?所以操作系统也管不了。那么一旦出现了D状态的进程,就相当于这个操作系统无法正常执行任务了。解决的办法只有两种:一是等这个进程处理完,而是掉电重启。

    4.Linux的进程管理

    进程之间是可以互相控制的。最典型的例子就是打开某个软件、关闭某个软件,我们无法直接隔着屏幕大喊一声:”我要关掉你!“这个软件就会关闭掉,而是需要通过一个进程给这个软件(也是进程)一个关闭信号,这样就形成了进程的互相控制。

    刚才提到了一个操作,叫做使用kill指令杀掉进程。实际上在以前,电脑还很贵的时候,那时候玩游戏都得看脸,经常各种程序死机,然后都会统一的调出任务管理器强制结束进程:

    那么Linux也有这样的操作,就是使用kill指令:

    kill -9 [PID]                <==通过PID去杀掉进程

    [-9]是什么?它是一个信号,这个信号代表的意思为强制中断一个进程的执行,也就是从内存当中删除这个进程。那么我在这里列举几个常用的控制信号,供大家作为参考: 

    代号名称内容
    9SIGKILL强制删除一个进程
    18SIGCONT继续一个进程的运行
    19SIGSTOP暂停一个进程的运行

    对于命令输入的格式,我们以杀掉某个进程举例,还可以这么写:

    kill -SIGKILL [PID]                <==与上面的效果等价

     那么我们还是以这段代码为例,演示这三个信号的使用以及观察不同信号下的进程状态:

    1. 1 #include
    2. 2 int main()
    3. 3 {
    4. 4 while(1)
    5. 5 {
    6. 6 printf("访问硬件啦!\n");
    7. 7 }
    8. 8 return 0;
    9. 9 }

     可以看到,随着不同的信号输入,进程的状态也随之变化。上图中我们观察到了前面所提及的进程的T状态,在这里得到了验证。不过有很多读者可能比较奇怪,为什么某些进程的状态会有一个'+'呢?有无'+'仅仅是为了区分是前台还是后台,这些信息是告诉操作系统的,而不是告诉用户的

    那么我们可以向进程发送多少种信号?我们可以通过这个指令来查看一共有多少种信号:

    kill -l                <==l是小写的L,查看所有的信号

    我们还可以发现一个非常麻烦的东西:因为[kill]这个指令是专门针对进程的PID的,所以每次使用[kill]命令时都得配合[ps]命令来查看要操作的进程的PID。那有没有什么办法,我们直接操作进程的名称而不是它的PID呢?——[killall]命令。我们使用[killall]命令重复一遍上面的操作:

    是不是方便许多呢?

    5.僵尸进程

    没有走错片场!朋友们!是的,没有听错,就是僵尸进程。僵尸是什么?尸体嘛!谁的尸体?进程的嘛!

    当有一个进程要退出的时候,它是直接原地消失、释放空间的吗?当然不是了,如果真是这样那么对于操作系统来说就非常奇怪了——刚才还好好的,回个头怎么人不见了?当进程退出的时候,它是不会立即释放空间的,它的PCB会保存一段时间让父进程或者操作系统读取,让父进程或操作系统知道这个进程即将退出了,然后父进程或者操作系统释放掉进程占用的资源和空间一般情况下,清理进程资源空间的操作都是父进程

    我举一个形象的例子:有一天进程A吃完饭没事干在内存里面溜达,突然看到进程B被一道指令给咔嚓倒在地上了,完了没呼吸了。那么进程A就只看见了内存里面躺着一具进程B的尸体,尸体不能凭空消失啊!此时进程A又看到了一个贼大的进程,这个进程脸上写着五个大字——B的父进程,然后就看见这个父进程对着B进程的尸体一顿处理,然后尸体就消失了,内存里面再也看不到B进程了。

    那么僵尸进程,指的是什么呢?就是进程退出时,依然会在内存里面待一段时间,如果父进程没有能力将此进程完整地释放掉,造成这个进程一直在内存里面,此时这个进程就是僵尸进程

    我们可以写一段程序,来探究僵尸进程是怎样产生的:

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 int main()
    5. 5 {
    6. 6 pid_t id = fork(); //创建一个子进程
    7. 7 while(1)
    8. 8 {
    9. 9 if(id == 0)
    10. 10 {
    11. 11 printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid());
    12. 12 sleep(1);
    13. 13 }
    14. 14 else if(id > 0)
    15. 15 {
    16. 16 printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
    17. 17 sleep(2);
    18. 18 }
    19. 19 }
    20. 20 return 0;
    21. 21 }

    让此程序运行,kill掉子进程,观察子进程的状态:

    可以看到我们的子进程已经是僵尸进程了,再重复一遍,僵尸进程产生的原因是:当进程要退出时,其占用的资源空间会在内存中保留一段时间,这段时间是专门让它的父进程来处理的;但是如果父进程一直没来处理,导致进程一直在内存当中,这个进程就是僵尸进程了

    为什么上图我们能看到僵尸进程呢?kill其他的进程而不会看到僵尸进程呢?这是因为我们代码的设计问题,我们仔细观察代码,我们设计父进程处理子进程"尸体"的行为了吗?所以产生僵尸进程的原因通常有三个:一是操作系统不稳定;二是代码写的不好;三是用户的不良操作习惯导致的

    僵尸进程通常是无法再进行管理的,所以我们不能直接杀kill掉它,而是交给操作系统来处理这个进程。如果连操作系统都无法解决掉这个僵尸进程,那就只能通过其他手段了,例如重装系统。僵尸进程的危害也是非常大的,它会导致内存泄漏

    6.孤儿进程

    很好理解吧?就是没有父进程的子进程。什么情况下才会发生?当父进程衍生出一个或多个子进程后,父进程先退出了,只留下了衍生出的一个或多个子进程,这些子进程就叫孤儿进程

    我们对上面的代码不进行修改,运行代码,然后直接kill掉父进程,观察子进程的状态:

    可以看到孤儿进程的变化就是其PPID变了,变成了1。我们再回想一下什么是PID:PID是操作系统加载进程时,给进程占用的内存空间一个标识符,这个标识符就叫做PID。那么操作系统是计算机开机的时候自动加载的,所以操作系统是第一个被加载到内存里的进程,所以操作系统的标识符为1,也就是操作系统的PID为1

    也就是说,我们先kill了父进程,就导致了子进程的父进程消失了,而操作系统绝对不会坐视不管,它会去领养这个子进程,所以我们可以看到,孤儿进程的父进程是操作系统。为什么操作系统要领养这个子进程呢?因为如果不领养这个子进程,它就会一直占用内存的空间,它不依赖任何父进程存活,这是非常危险的事情,因为我们无法控制和管理它了!这就导致内存泄漏了

    7.进程优先级

    操作系统是有强迫症的,它不允许任何扰乱秩序的现象发生,什么是优先级?优先级决定了这个进程是先被cpu处理还是后被cpu处理。我们可以试想一下,如果所有的进程同时被唤醒,那么cpu应该先处理哪个进程?所以规定了进程必须有优先级的概念。当然这只是优先级为什么存在的一部分原因。

    我们举个例子:如果没有优先级的概念,所有的进程PCB都放在cpu的运行队列上,就像我们在食堂窗口打饭一样,每个人都是按照顺序排队打饭,当食堂的饭菜都打完了之后,此时食堂的工作人员需要为食堂添加饭和菜,但是因为没有优先级,所以这些添加饭菜的工作人员必须排在队伍的后面,而排在前面的人又因为没有打到饭,就不会离开窗口。如果拥有优先级的概念,那么我们在窗口的旁边开一个小门,取名“工作人员专用通道”,此时如果食堂窗口没有饭菜了,那么添加饭菜的工作人员可以直接提着饭菜从这个小门进入到食堂内添加饭菜,然后排队打饭的人就可以继续运转了。

    所以在进程中,有许许多多对于操作系统来说非常重要的进程,这些进程的优先级往往很高

    所以优先级简简单单的排队摇号一样,而是操作系统对进程规定的一套规则。在这套规则之下,进程被cpu合理的调度,维持操作系统的正常工作。

    那么在Linux下,如何查看某个进程的优先级呢?首先当然是输入上面介绍过查看系统进程的指令啦!然后就是寻找[PRI]关键字

     看到了吗?PRI下面的数字就是进程的优先级,这个PRI值越低,就说明进程的优先级越高。因为PRI值是由操作系统内核动态调整的,我们无法直接去调整这个值,所以我们必须通过nice值去调整它。nice值就是上图PRI后面NI。这个调整过程有点类似与内核与shell的关系。

    那么因为PRI是系统内核去动态调整的,所以我们修改之后的PRI值需要经过内核的“同意”,如果这个PRI值超过了内核的最大限度,那么这个值就会保留在临界值。

    我们调整PRI的计算规则为:新的PRI = 进程默认PRI + nice值,这个nice值有正有负。什么意思呢?上图的PRI我们没有做过任何修改,那么第一个名为systemd的进程的默认PRI就是80,此时我们给nic的值为-10的话,那么新的PRI计算规则就会遵从上述的公式,即新的PRI为70。

    如何调整nice值呢?我们最好还是对自己写的程序进行操作,老样子,写一个死循环的代码:

    1. 1 #include
    2. 2 int main()
    3. 3 {
    4. 4 while(1)
    5. 5 {
    6. 6 printf("访问硬件啦!\n");
    7. 7 }
    8. 8 return 0;
    9. 9 }

    运行此程序然后查看此程序的PRI值:

    然后我们通过下面这条命令来修改已存在的进程的nice值:

    renice [number] [PID]                <==number为想要的nice值,PID为要操作的进程

     8.进程的四个重要概念

    有了上面的介绍后,我们对进程的理解就有了初步的认识,那么现在,就来介绍一下进程当中的四个重要概念。

    • 竞争性:因为cpu资源优先,所以进程难免会存在竞争行为,具体体现在优先级上。
    • 独立性:进程运行期间,各个进程是不会相互干扰的,即使是父子进程。
    • 并行:当有多个cpu时,这些cpu同时处理多个进程的行为叫做并行。
    • 并发:在一段时间内,每个进程都可以被cpu处理一部分指令,这种行为称为并发。

    前两个概念不需要介绍,仅凭字面理解也能知道其含义。但是并行和并发是什么东西呢?我们先来感性的认识一下并行。

    假设cpu处理一个进程的时间为1秒,那么1个cpu处理99个进程的时间就是99秒。但是当有一台拥有3个cpu的计算机处理这99个进程时,只需要33秒。这就是并行,多个cpu同时处理多个进程

    那么我们的计算机都只有一个cpu啊,但是我为什么感觉我的进程是被同时处理的,我能边看视频边写博客还边打游戏啊!实际上我们不要低估cpu的计算能力,cpu处理指令的速率是纳秒级别的。这时候就需要介绍我们的并发了。

    事实上,每个进程在运行的时候都有自己的时间片。什么是时间片呢?就是每一次运行能被cpu处理多久。为什么叫每一次呢?因为我们的进程不是一次一口气被cpu处理完的,而是分批次进行处理。假设有进程A、进程B、进程C在运行队列中,假设这三个进程的时间片都是10毫秒。那么此时轮到进程A被cpu处理,处理10毫秒后,不管进程A还剩下多少行指令没有被执行,操作系统都会直接把进程A放到运行队列的最后,然后让cpu去执行进程B的指令,对于进程B也是像进程A一样,执行完10毫秒后被操作系统放在运行队列的最后,进程C同理。我们不要觉得这样很麻烦,我们试着算一下,一个进程的时间片是10毫秒,那它一秒可以被执行100次,这是很快的。把进程放到队列的最后的这个行为,叫做进程切换。我们谈谈进程切换时候,cpu、操作系统发生了什么事。

    每个cpu都有一套寄存器,注意是一套而不是一个。那么有一些关键寄存器会存储当前指令的下一条指令的地址,进程在被cpu处理的时候,一定会产生一些临时数据,这些临时数据也被放在某些寄存器中,并且这些临时数据只属于当前进程。这些临时数据是干嘛的呢?如果我们有一个进程是执行1+1这个算法,那么是不是要有一个寄存器存储1+1的结果,然后作返回值呢?唉正当cpu要执行返回值这条指令的时候,操作系统告诉我们这个进程的时间片到了,必须放到运行队列的最后了。此时这些临时数据是不能被丢到的,因为我们的进程还没执行完呢!所以操作系统就会帮忙把这些临时数据(包括关键寄存器存储的当前指令的下一条指令的地址)转存到操作系统的某个位置当中。当下一次cpu再次处理我们的进程的时候,操作系统一比对,发现此进程是上一次没执行完的, 放了一些临时数据在我这里,现在又要被cpu执行了,那么操作系统就会把这些数据放回寄存器,但后cpu根据这些数据继续执行我们的进程。

    把临时数据转存到操作系统的行为叫做上下文保护,把临时数据写回寄存器内的行为叫做上下文恢复。

    9.环境变量

    环境变量是对操作系统有一定特殊功能的变量。

    我们举个例子,就拿我们对Linux的理解:指令是不是程序?是的。我们自己编写代码,然后编译链接生成的可执行文件是不是程序?是的。那么问题来了,为什么我们运行指令的时候直接在命令行输入指令呢?为什么我们运行自己写的程序的时候需要自己指定位置呢

    运行程序的时候,需要知道此程序在哪个位置,然后才能执行。那么Linux的指令为什么不需要指定其指令的位置呢?这是因为有环境变量[PATH]的作用,这个[PATH]变量里面记录着指令(一般都是指令)的位置假设我们执行[ls]这个指令时,操作系统就通过[PATH]变量里面的路径去查找[ls]的位置,然后执行。如果[PATH]变量没有记录我们输入的命令的位置时,就会显示[command not found]的错误信息。

    因为[PATH]变量没有记录我们输入的指令的位置信息,所以我们必须手动指定指令的位置。那么我们可以总结出指令(程序)是如何执行的了:

     那么如果我们想要在执行我们自己写的程序时,不指定其位置,应该怎么办呢?方法一,将我们的程序拷贝至[usr/bin/]目录下,其原因在于[PATH]变量记录了这个目录。方法二,将我们自己程序的位置信息导入到[PATH]变量中。我们更加推荐方法二

     在将位置信息导入[PATH]变量之前,我们必须先知道[PATH]变量里面有什么,也就是如何查看[PATH]里面的内容。我们会用到下面的指令:

    echo $PATH                <=='$'是不能少的

    可以看到,操作系统提供的指令的位置,已经被写入[PATH]变量里面了。其中的[:]就是分隔符,也就是冒号之前是一个路径,冒号之后是一个路径。我们需要注意:[PATH]变量是操作系统启动时自动生成的一个变量,也就是说,当我们修改[PATH]时操作失误了,只需要重启一下系统即可。

    那么我们如何向[PATH]添加一个路径呢?我们用到下面的指令:

    export PATH=$PATH:[自己的路径]                

    我们试着将我们自己的程序导入环境变量:

    我们仅仅介绍了一个环境变量,那么到底有多少个操作系统呢?我们可以使用下面这条命令:

    env                <==查看当前shell环境下的环境变量与内容

    此时我们知道指令是怎么执行的了,也知道环境变量是随着启动操作系统时生成的,也就是说,环境变量是属于bash的

    指令是一个程序,在bash上执行,那么这个指令就是bash的子进程。我们常用的指令[pwd]能够显示我们当前用户所在的路径。那么这个[pwd]指令是如何知道我们在哪个路径的?这里我告诉大家,有一个环境变量叫做[PWD],这个环境变量存储着用户当前的所在位置,大家可以在上图找一找[PWD]这个环境变量。那么我们就知道了,执行[pwd]指令时,操作系统会从[PATH]这个环境变量找[pwd]这个指令所在的目录,然后执行,所以[pwd]指令能够显示当前用户所在的路径就跟[PATH]变量没有关系了,而跟[PWD]有关系了。这个关系体现在哪里?当然是在程序内部啦!我们可以使用[PWD]这个环境变量自己实现一个[pwd]指令

    1. 1 #include
    2. 2 #include //获取环境变量函数的头文件
    3. 3
    4. 4 int main()
    5. 5 {
    6. 6 char* ret = getenv("PWD"); //获取环境变量的内容
    7. 7 printf("%s\n",ret);
    8. 8 return 0;
    9. 9 }

    然后我们运行此程序:

     这样就自己实现了一个[pwd]指令。当然我们也使用了一个函数,我们可以配合[man]指令浏览这个函数是什么:

    [PWD]环境变量既然能存储当前用户的所在位置,就说明[PWD]环境变量是随着当前用户的位置变化而变化的

    刚才我们提到了,我们在bash上运行的程序,是bash的子进程,而环境变量是属于bash的,子进程为什么能用父进程的环境变量?这是因为,子进程可以继承父进程的环境变量!并且,环境变量一定是全局属性的!

    子进程是如何继承环境变量的?我们动脑筋想一想,子进程是不是有一个主函数?这个主函数我们平时使用时是没有参数的,但实际上它是可以带参数的!还能带三个

    那么主函数如果带参数的话,我们一般这么写:

    1. 3 int main(int argc,char* argv[],char* environ[])
    2. 4 {
    3. 5
    4. 6 }

    第一个参数代表的意思为:指令参数的个数(包括指令);第二个参数代表的意思为:指令参数的指针数组(因为指令参数是一个字符串);第三个参数代表的意思为:环境变量的指针数组(因为环境变量是一个字符串)。但是第三个参数我们一般不用!而是使用操作系统提供的外部的指针数组指针[char** environ]或者是系统提供的接口函数getenv()

    有了这个认识,我们使用前两个参数来完成一个任务:判断当前用户是否为[root],如果是[root]就执行某某指令;如果不是则报错。那么代码我们可以这么写:

    1. 1 #include
    2. 2 #include
    3. 3 int main(int argc,char* argv[])
    4. 4 {
    5. 5 if(argc < 2)
    6. 6 {
    7. 7 printf("指令参数太少!\n");
    8. 8 return 1;
    9. 9 }
    10. 10 if(strcmp(argv[1],"-a")==0)
    11. 11 {
    12. 12 printf("执行-a\n");
    13. 13 }
    14. 14 else if(strcmp(argv[1],"-b")==0)
    15. 15 {
    16. 16 printf("执行-b\n");
    17. 17 }
    18. 18 else
    19. 19 {
    20. 20 printf("指令有误!\n");
    21. 21 }
    22. 22 return 0;
    23. 23 }

    我们的可执行文件名为[main]。执行此文件:

     怎么样?是不是非常神奇?如果还有问题我们就打印这个两个参数,看看它到底是个什么东西:

    1. 1 #include
    2. 2 #include
    3. 3 int main(int argc,char* argv[])
    4. 4 {
    5. 5 printf("%d\n",argc);
    6. 6 int i=0;
    7. 7 for(i=0;i
    8. 8 {
    9. 9 printf("%s\n",argv[i]);
    10. 10 }
    11. 28 return 0;
    12. 29 }

    看见了吧![argc]是存储指令参数的个数的(包括指令),[char* argv[]]这个指针数组是存储指令参数的(包括指令)

    不要纠结,这些传参的工作是操作系统完成的

    那么对于第三个参数,想必不用我多说,它就是一个指针数组,存储的是各个环境变量的内容,因为这些内容是字符串常量,而表示字符串常量通常使用其首字符地址,所以第三个参数就是一个指针数组啦!我们是很少使用第三个参数的,因为这个数组存储了所有的环境变量,想要找到特定的环境变量还是挺困难的,那么我们使用这段代码,证明第三个参数存储了环境变量:

    1. 1 #include
    2. 2 #include
    3. 3 int main(int argc,char* argv[],char* environ[])
    4. 4 {
    5. 5 int i=0;
    6. 6 for( i=0;environ[i];i++)
    7. 7 {
    8. 8 printf("%s\n",environ[i]);
    9. 9 }
    10. 33 return 0;
    11. 34 }

    亦或是另一种写法:

    1. 1 #include
    2. 2 #include
    3. 3 int main(int argc,char* argv[])
    4. 4 {
    5. extern char** environ;
    6. 5 int i=0;
    7. 6 for( i=0;environ[i];i++)
    8. 7 {
    9. 8 printf("%s\n",environ[i]);
    10. 9 }
    11. 33 return 0;
    12. 34 }

    刚才也提到了,环境变量是具有全局属性的,也就意味着子进程只能继承父进程的具有全局属性的环境变量。什么?环境变量还有局部属性的!当然,但是我们并不这么称呼它,而是称作本地变量。如何设置本地变量呢?我们只需要在bash上面按这个格式敲指令:

    [变量名]=[内容]                <==这里的等号两边一定不能有空格!

    我们来设置一个本地变量:

     我们发现一个奇怪的现象,使用[env]命令查看我们设置的变量,并不能显示出结果,就证明我们这是的变量是本地变量。但是使用[ehco]命令便可以查看到,为什么?实际上[echo]是可以操作环境变量的,但这并不说明它只能操作环境变量,这里也给我自己提个醒:[echo]命令是可以操作所有的变量的,不管是本地变量还是环境变量

    还记得刚才写的查看环境变量的代码吗?我们再执行一次这个程序,看看我们设置的变量是否被继承了:

    可以看到,子进程并没有继承父进程的本地变量。那我们如何使本地变量变成环境变量呢?我们输入下面这个指令:

    export [变量名称]                <

     现在我们学会了如何设置本地变量和如何把本地变量转换成环境变量了。那么如何查看本地变量呢,或者说如何查看所有的变量呢?我们使用下面这条命令:

    set                <==查看bash的所有变量

     这条指令输出的内容是非常非常非常多的!我们配合管道命令:

    那么如何取消变量呢?我们使用下面这条命令: 

    unset [变量名]

  • 相关阅读:
    Django 自定义用户 VS 用户资料
    xcode常用功能与操作
    金和OA SQL注入漏洞
    接口自动化测试之 —— requests模块详解!
    鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
    React之Hooks基础
    博途PLC 1200/1500PLC开放式以太网通信TSEND_C通信
    【Python、Qt】使用QItemDelegate实现单元格的富文本显示+复选框功能
    怎么把PDF转换为PPT格式?分享三种简单的转换方法
    Layui实现之登陆页面&&实现扩展模块
  • 原文地址:https://blog.csdn.net/weixin_59913110/article/details/127736705