0


初始Linux—Linux系统编程第三节——初始进程

冯 · 诺依曼体系结构

在说冯诺依曼体系结构之前,我们先来了解这么一个常识:

我们的电脑或者手机,

总的来说,其体系结构都是由 **软件+硬件 **构成。

而硬件部分,有 像我们所说的磁盘、键盘、网卡等等硬件设施构成整体的硬件框架结构。

而软件部分,最核心、最重要的,就是我们的操作系统了

软硬件结合,构成我们的计算机的体系结构。

就像下图所示:

那么,我们的计算机是怎么将软件和硬件结合在一起的呢?

这个时候,我们就需要来了解一个体系结构——冯诺依曼体系结构

我们基本所有的计算机系统,都遵循这样这么个结构。

这个结构其实也很简单,其可以用图示表示成这样:

首先,我们可以知道,

输入设备一般可以是包括 键盘,硬盘,网卡,鼠标,扫描仪, 写板等
输出设备一般可以是包括 显示器,打印机,硬盘,网卡等

解释一下,这里有的设备既可以作为输入设备,也可以作为输出设备。

比如刚刚所举的 网卡,硬盘 等。其实很好理解,待会我们下面会举例。

而这里的存储器,指的就是内存

运算器,可以简单理解为就是用于计算的那些东西(简单举例子,就是加减乘除等等)

控制器,可以理解为 作用是控制着 数据信号 传导

我们一般习惯把 输入设备和输出设备统称为 外设

我们从这个图中可以得到这样一些看似简单,但是很有用的信息:

  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,**不能访问外设(**输入或输出设备)
  • 外设(输入或输出设备)要输入或者输出数据****,也只能取写入内存或者从内存中读。
  • 一句话,所有设备都只能直接和内存打交道。

说的直白一点,CPU是内部,而外设是外部,内部和外部要建立联系,那就必须要经过哨兵——内存。

再或者说,CPU是女方,外设是男方,而男方和女方想要牵手成功,就需要媒婆,而该内存就是扮演了媒婆这样一个角色。

理解到这个意思就行。

那有人会问,为什么要有内存呢?

难道就不能让CPU直接从外设读取信息,然后处理吗?

我们来回答一下这个问题:

我们结合这一张图来说明:

在金字塔顶端的说明其价格贵,在底端的说明其价格便宜。

在金字塔顶端的说明其运算速度快、效率高,在底端的说明其运算速度较慢、效率低。

我们讲得更加直白简单一点:

在CPU中,运算速度都是以纳秒为计算单位的;

在内存中,运算速度是以微秒为计算单位的;

在硬盘(SSD固态硬盘等)中,运算速度是以毫秒为单位的。

如果没有内存,那么CPU的运算速度是那么高,但是呢,硬盘的运算速度相较而言又是那么低,你们猜,最终的运算速度是要依照谁来定呢?

当然是硬盘。

这就类似于木桶原理。

可能CPU在运算的时候,0.001%的时间用来计算,99.999%的时间都是在用来等待了。

这样的话,不仅计算的效率变低,而且由于大部分的时间都在用来等待,会造成极大的浪费。

可是CPU又是那么的贵,所以CPU就总是只有那么一点点。

这样,便诞生了内存。在性价比的方面,做了一个折中。

这样,使得计算机的运算效率不仅不会那么低,并且还使得计算机不会那么贵,让普通人也能够用得起。

当然,在CPU中还有着寄存器、缓存等概念,它们的主要作用也同样是提高运算效率来的。

就好比你跟不上我,那我就先放在缓存(或者寄存器),然后我继续做我的事,不至于让我在原地静静地傻等。

好。我们解释完了该体系中的相关的常识性概念之后,我们再来用动态的眼光,看看这个体系结构:

我们先从硬件的角度来看:

想要数据信号从输入设备进入到输出设备中,

我们可以认为:

其先是经过了输入缓冲区(对标你的scanf在输入一行数据的时候,其就是先加载到了输入缓冲区中),当你敲下回车的那一瞬间,缓冲区刷新,数据就被加载到了内存(也就是存储器)中。(当然,刷新缓冲区的方式不仅仅有按回车这一种方法)

然后内存再将数据传给CPU,让CPU处理。

数据信号从CPU中出来,先进入内存当中,然后先是进入输出缓冲区中,也叫预写入

当程序终止、或者是遇到 ‘\n' 、fflush函数 等时,缓冲区刷新,就会将数据载入到输出设备当中。

简单比划一下就是这样:

如果从软件的角度来去看,这个工作是由谁来做的呢?

答案是:操作系统。(Operator System 简称OS)

也就是说,是操作系统完成数据的加载、输出等等工作。我们接下来,就会详细地介绍它的作用和功能。

这也从另一个角度来说就是:从软硬件的角度,内存的存在、缓存的特性都是可行的。

我们来通过举一个例子的方式,即解释从你登录上qq开始和某位朋友聊天开始,数据的流动过程。

我们忽略网络细节。

简单图示如上。请看下面的详细过程:

当你输入一条消息,没有发送的时候,可以认为其实在硬盘或者是输入缓冲区中,消息发送后,其 先会载入到内存中,(这其实也与你的QQ一直都是处于运行状态,即一直在内存运行中相匹配)紧接着,经过你的电脑的CPU处理,再经过内存,输出到你的网卡当中;

然后,通过网络(这里我们先忽略网络相关细节),传送到你的同学的电脑的网卡当中。数据通过你同学电脑的网卡,加载到你同学电脑的内存当中,经过CPU处理,再经过内存,输出到你的同学的电脑的显示器上。

在这里,对于你的电脑的主机而言,你的键盘相当于输入设备,网卡相当于输出设备;

你的同学的电脑而言,他的网卡相当于输入设备,而显示器则相当于输出设备。

好,对于冯诺依曼体系结构,我们暂时先说到这里。

操作系统:Operator System(OS)

我们下面继续来说操作系统。

首先,我们需要了解什么是操作系统:

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统来说,其包括内核(进程管理,内存管理,文件管理,驱动管理)和其他程序(例如函数库, shell程序等等)

说人话,就是操作系统是一套用来搞管理的软件。

管理什么?

管理软硬件

-> 硬件:包括冯诺依曼中的所有设备;

->软件:安装、卸载等,在系统层面,其包括文件、进程、驱动;

下面是一个比较好的管理分层的图的例子。

我们结合这一张图, 来举一个例子:

我们把操作系统看成是校长,底层的硬件看作是学生,那么驱动程序的存在就相当于是充当了导员的角色。

校长和学生,一般不见面,通过导员进行管理。但是,校长(操作系统)可以通过导员(驱动系统)拿到学生(底层硬件)的信息。

在这简单的层级关系里,校长(操作系统)就是来管理的,管理整个学校(整台计算机系统),用有决策权——简单理解可以认为是学生在哪一个班,正常上学或者勒令退学(对内存中的进程、文件等掌有生杀等等大权)

而当信息量和庞大的时候,校长(操作系统)就会将所有的学生的信息都描述起来(由于LInux就是用C写的,所以其用的就是结构体来完成的),然后可以通过链表等数据结构的方式,来将这些学生组织起来。

用6个字,就是:

先描述,再组织。

总结 一下,就是要:

描述被管理对象
组织被管理对象

1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构

最后补充一点:为什么要存在OS,OS的存在有什么样的意义。

OS是为了与硬件交互,管理所有的软硬件资源为用户程序(应用程序)提供一个良好的执行环境这样的需求而诞生的。你想一下,如果没有OS,你打个lol,2s退出一次,1s电脑重启一次...你还玩个锤子哈

再简单说一下系统调用和库函数的概念,这个我们后面还会继续说:

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。说人话,就是有操作系统给我们提供的接口。(比如我们下面的fork函数,后面将要学习的exec系列的函数等)

系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。说直白点,就是我们的prinf,scanf等等,都算是这样经过二次开发后提供给用户使用的。

进程的基本概念

课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体

还是说人话,我们从两个方面去解释:

1、类比刚刚操作系统的管理,那么操作系统对于进程的管理,

依然遵循六字原则:先描述,再组织。

2、对于一个普通的文件而言,其有文件属性(即文件大小、所有者、创建日期等等)+文件内容。

那么类比于进程,其也是包含了两方面:进程属性+对应的文件。

结合我们刚刚说的,一个程序文件加载进了内存,就变成了进程。

那么这个进程是就会被操作系统管理。

如果同时存在了多个进程,那么这些进程就会通过”先描述,再组织“的方式被管理。

那操作系统是如何来描述这些进程的呢?

答案是:还是用一个结构体来完成。其有专门的名称——PCB(Process Control Block)

通过查看Linux源码,我们会发现,有一个叫做task_struct的结构体,专门用来完成该项工作。

PCB相较于task_struct的概念,就好比这样(如下图)

那么,我们现在对于 进程的认识可以这样来说明:

进程,就是可执行程序和需要管理进程的数据结构的集合

task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据。(我们之后会再说)
  • I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

接下来,我们就要详细地挑选task_struct里的重要内容来进行讲解。

不过在此之前,首先,我们来说一下,如何查看进程:

有两种方式都可以:

1、进程的信息可以通过 /proc 系统文件夹查看

如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹

2、大多数进程信息同样可以使用top和ps这些用户级工具来获取

举个例子:

我们写一个死循环好了,目的就是让该程序一直运行下去。

然后让其编译运行,就会在屏幕上不停地打印hello Linux!

我们复制一个会话

这样,等会程序运行的时候,我们就能够检测到了。

好,现在我们让程序开始运行。

解释一下,这里,我们是用ps指令,来去查看进程的状态

关于ps的选项,参见下图:

然后后面的grep就是过滤一下要或者不要的信息。

读者可以自行尝试一下如果不过滤会出现什么样的效果。

关于程序进程的相关选项,我们下面会说。

进程标识符

对于一个进程,OS为了能够方便识别它们,在创建PCB的时候,每个人都会给其一个id。这就是进程标识符。

对于标识符,我们有pid和ppid之分。

pid指的是子进程,

ppid指的是父进程。

我们在程序中可以通过getppid() 和 getpid() 来实现。

我们还是通过一个小程序来举例:

(运行结果)

我们不难发现,这里的ppid和pid都是一串数字,实际上就是编号。

其中ppid指的是父进程,pid指的是子进程。

我们等会可以结合下面fork函数的例子来看。

通过系统调用创建进程-fork初识

我们这里仅仅是了解一下,fork怎么用,达到会用的标准就行。

先来介绍:

我们输入

man fork

可以看到,fork没有参数,返回值为pid_t的类型,作用是创建一个子进程。头文件为<unistd.h>

实际上,我们需要知道,fork是有两个返回值的。

为什么呢?

我们可以这样来理解一下:

同时,需要注意:子进程的返回值是0;而父进程的返回值是子进程的id。

我们来看这样一个程序(来看实操):

总共9行代码

我们来看运行结果。

我们会发现,第二个printf的内容执行了两次。因为子进程在fork里创建后,子进程和父进程都会执行第二个printf。

也就是说,子进程和父进程是独立运行的。

那我们再来举一个例子,来看:

那么这下我们让代码执行,其会产生什么结果呢?

由于父子进程我们无法准确让谁先跑,谁后跑,所以我们加上一个sleep来以示区分。

我们会发现一个现象:子进程的ppid就是父进程的pid。这样也就能说明一个问题——为什么有父子这样的叫法了。

那父进程的ppid又是谁的呢?

答案是-bash的,就是操作系统的。(bash相当于操作系统手下的一个助手)

而bash也是一个进程。如下图

那bash能不能挂?

当然不能!bash是不能挂的!

而由bash通过创建子进程的形式,和我们刚刚用fork创建的子进程的形式是基本一致的。

进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)
我们可以来看一看Linux内核源代码里是怎样定义的:

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

R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。(解释一下,R状态表明的就是运行状态,但其不一定表明的就是正在运行)

S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。(也就是等待状态)

D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。(处于D状态的操作系统是杀死不了的,而如果大量的程序由于在IO而进入D状态,很有可能会使整个服务器崩溃——过年抢红包,偶尔的王者荣耀的服务器崩溃就和这有关)

T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

我们可以通过这样的指令来查看进程状态:

ps aux 
ps axj  //上面的或者下面的

像我们刚刚看到的,比如:

通过这里的我们能够看到,我们这里的hello程序是处于一个休眠的状态。

这与IO等待有关系,因为IO是要和硬盘显示器发生交互,前面说过这个过程是很慢的, 就是说其0.1秒在运行,0.9秒都在等待,所以当你在查看其状态的时候,其处于S状态——即休眠状态。

我如果把printf的内容去掉,就是让它一直死循环,就直接这样:

我们再来看:

我们让其运行,

我们看到,其为R状态,就是可以理解为是running 状态。这里的+表示其实在前台运行的,就是说我按 ctrl c 可以将程序终止

如果我要在后面加上&,其就变成是在后台运行的了。

这个时候,我们再按ctrl c,然后再调用后台观察,我们可以看到,R后面的+小时了,并且无论我们怎么按,其都是无法停止。

这个时候,我们如果想要终止它,可以用kill命令

后面的27459就是当前进程的进程编号。

我们这个时候再看,就会发现进程没了。

这个图,可以参考一下,看中文就可以了。

我们再来说两种特殊的进程——僵尸进程和孤儿进程

僵尸进程

——特殊的Z状态:

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

我们来举一个例子:

可以看到,按照我们的思路,子进程会在5秒之后退出,然后父进程是一个死循环。我们来看看这两个的进程在运行过程中的运行状态是怎样的。

我们编译后 ./hello让其执行

在另外的 一个窗口中,我们制作一个脚本,用于每1秒查看其进程状态

脚本如下:

while :; do ps aux | grep hello | grep -v grep;sleep 1; echo "#################";done

我们从上面的监视可以看出,子进程一开始和父进程一样,都是S状态,几秒钟后,其变成了Z状态。这里的Z就是僵尸状态。

那么为什么会有僵尸状态呢?

原因很简单。我们在创建一个进程的时候,是操作系统创建的。但是,进程在结束时,也需要有人来“收回”。

所以说,这里的子进程就是在等着父进程将其收回,读取它的退出状态。所以一直就是处于僵尸状态。

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。

可父进程如果一直不读取,那子进程就一直处于Z状态?是的!

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!

那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的。

因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。

所以僵尸状态会导致什么?

就是我们之前强调很多遍、耳熟能详的——会导致内存泄漏。

那如何避免呢?实际上可以用wait。这个我们后面再说到进程等待的时候再说。

孤儿进程

孤儿进程,实际上和僵尸进程是两个方面,结合“孤儿”以及我们刚刚所说,不难猜出,僵尸进程是父进程一直在干活,子进程先挂了;那孤儿进程就是父进程先没了。

我们不再做过多的赘述,简单理解就是父进程没了,子进程还在。

有兴趣的读者可以自行尝试一下。

那么孤儿进程难道就没有父进程了吗?

答案并不是这样的。

这个时候,往往操作系统会来帮你。

将你的孤儿进程领养。

被谁领养?

一般都是一号进程。

孤儿进程被1号init进程领养,当然要有init进程回收喽。

进程优先级

我们说,进程的运行和办事情一样,也是有先后缓急之分的。

哪个进程先执行,哪个进程后执行,这是由其优先级所决定的(注意和权限区分一下,权限是能不能的问题,而优先级是已经能的基础上先后的问题)

我们如果输入ps -l,

会看到这样的信息:

这里的PRI是最终的PRI,是影响优先级的重要因素。一般而言,PRI越小,优先级越高,PRI越高,则相反。

解释一下其他几个有价值的值是什么意思:(其实我们部分在上面已经说过了)

UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值

而PRI是怎么算的呢?

我们可以这样认为:

PRI (最终) = PRI(开始)+ NI(这里的NI意为nice值)

而PRI(开始)的值基本上都是80。我们在上面看到的PRI是最终的PRI

这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行

所以,调整进程优先级,在Linux下,就是调整进程nice值

nice其取值范围是-20至19,一共40个级别。

需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。

可以理解nice值是进程优先级的修正修正数据

我们可以通过top命令进入top后按“r”–>输入进程PID–>输入nice值 的方式,来将已有的nice值进行修改。

我们这里再补充一下其他的概念:

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
**独立性: **多进程运行,需要独享各种资源,多进程运行期间互不干扰
**并行: **多个进程在多个CPU下分别,同时进行运行,这称之为并行
**并发:**多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

再说一点就是,每一个进程在CPU上运行的时候,都会有一个时间片。当时间片到的时候,进程就会从CPU上被强行扒下来。

环境变量

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数

就比如说,在Linux系统中,我们知道,ls、touch等指令,实际上也是一个个可执行程序。但是当我们输入ls指令,touch指令啥的,为啥可以不带路径?

这就和环境变量有关了。

环境变量中有一个PATH项:

而我们当需要执行ls touch 等指令的时候,会优先去这些路径下寻找。

如图:

也就是说,这些指令早已经被存储了起来,即存储到了这些路径下的文件夹下。并且这些路径早已经被当成环境变量,它可以认为是整个操作系统调用指令的“全局变量”。、

我们输入env,可以看到我们所有的环境变量

那这么说,如果我将我的可执行程序添加到环境变量里,是不是就可以不带路径直接执行了呢?

确实如此。

不过一般不建议这么做,因为会污染原有的环境变量。

那么我们最起码要说说如何创建、删除环境变量的吧

和环境变量相关的命令

1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量

就直接在命令行中去试试就可以了。我在这里就不试了,感兴趣的读者可以自行去尝试一下。

环境变量的组织方式

就这么一张表,相信能看懂。意思其实很简单,其就是用指针数组的形式来存储的。

这是我们在命令行中所说的一些关于环境变量的内容。

那要是在程序中呢?

我们在这里,先说一个知识点,叫做main函数的参数。

main函数的三个参数

可能很多读者会看到书上曾经写过main函数的参数,实际上,main函数有三个参数,只不过我们平时不写,系统已经帮我们默认了。

我们借此机会,将其讲解一下:

这三个参数是这样的:

int main(int argc, char* argv[], char* env[])

其中,前两个是命令行参数,最后的那个是环境变量参数。

我们来通过例子的方式讲解:

我们创建一个文件myfile.c,这是代码:

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

然后我们正常编译,来看:

如果我们这样去运行:

得到的就是这样。

如果我们这样去运行:

得到的结果是这样:

如果我们这样运行,得到的结果是这样:

以此类推,在此就不过多举例了,我们已经能够发现这其中的规律了:

就是在命令行中,我们怎样去执行的,那么我们就会看到怎样的结果。

也就是说,我们加了多少个-x选项,这里的argc就是其个数。

而argv[i]存储的正是每一个参数的-x选项的内容。

如果没有带选项,那么默认就是我们的文件名。

这个用法实际上让人很容易联想到strtok函数(这个我们后面再说)

那有什么运用场景呢?

可以想一想,我们的ls后面的那些选项,内部的逻辑是不是会存在着if(argv[i] == '-a')这样的逻辑呢?

好啦,前两个变量说完了,它们都是命令行参数,第一个是和程序执行时后面带着的 -x 的个数有关系(x可以为a,b,c...),而第二个参数正是存储每一个 -x 参数的(每一个-x都是一个字符串,它们的首元素地址存储到指针数组argv中)。

那后面的变量?

就是环境变量参数?

是的!

我们来看这样一段代码:

我们可以看到,其将我们的环境变量全部打印了出来。

这和我们刚刚在命令行中直接用env打印出来的是一样的。有兴趣的小伙伴可以一试。

同样,也可以用第三方变量environ获取、

就像这样:

这样后运行的结果和刚刚的是一模一样的。这里的environ可以理解为存储着指针数组的二级指针。

通过系统调用设置环境变量

我们同样还可以通过系统调用设置环境变量

通过这样,我们就可以获得环境变量PATH。

运行后和我们刚刚直接 echo $PATH 的结果是一样的

另外,我们再强调一下,环境变量是具有全局性的,也就是说,环境变量也是可以被子进程继承下去的。

好啦,本节内容到此就结束啦~~~记得关注呦😙

标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/xdnxl/article/details/122871236
版权归原作者 jxwd 所有, 如有侵权,请联系我们删除。

“初始Linux&mdash;Linux系统编程第三节&mdash;&mdash;初始进程”的评论:

还没有评论