【Linux进程概念】:冯 • 诺依曼体系结构 | 操作系统 | 进程 | fork | 进程状态 | 优先级 下

简介: 【Linux进程概念】:冯 • 诺依曼体系结构 | 操作系统 | 进程 | fork | 进程状态 | 优先级

三、创建子进程fork

上面我们写了一个死循环代码,然后 “ ./ ” 运行,一般我们称之为命令式创建进程,实际上我们也可以用代码来创建子进程。

fork 也是系统调用接口,对于 fork 我们还会在 “ 进程控制 ” 章节中再深入,在此文中我们会通过 a) 程序员角度。 b) 内核角度。来学习 fork。

💦 认识 fork

通过命令 man fork 来查找 fork 的相关手册:

💦 使用 fork 创建进程

这里 fork 后,后面的代码一定是被上面的父子进程共享的,换言之,这个循环每次循环都会被父子进程执行一次(去循环已验):

可以看到子进程的父进程正是父进程的 pid,换言之,谁调 fork,谁就是父进程,父进程通过 fork 创建了子进程:

使用 ps ajx 命令来查看当前进程:

💦 程序员角度理解 fork

通过上面的代码知道了 fork 是创建子进程,也就意味着 fork 之后,这个子进程才能被创建成功,父进程和子进程都要运行,但是 fork 之后,父进程和子进程谁先运行,不是由 fork 决定的,而是由系统的调度优先级决定的。

也就是说父子进程共享代码 —— 只读的,不可修改或不可写入的;而用户数据各自私有一份 —— 比如使用任务管理器,结束 Visual Studio2017 进程,并不会影响 Xshell,一个进程出现了问题,并不会影响其它进程,所以操作系统中,所有进程是具有独立性的,这是操作系统表现出来的特性。所以将各自进程的用户数据私有一份,进程和进程之间就可以达到不互相干扰的特性。

注意这里私有数据的过程并不是一创建进程就给你的,而是采用写时拷贝 的技术,在 C++ 里我们和 深浅拷贝 一起谈过,这里后面我们再详谈,因为我们虽然在语言上学过了,但是在系统上还没学过。

💦 内核角度理解 fork

fork 之后,站在操作系统的角度就是多了一个进程,以我们目前有限的知识,我们知道 进程 = 程序代码 + 内核数据结构(task_struct),其中操作系统需要先为子进程创建内核数据结构,在系统角度创建子进程,通常以父进程为模板,子进程中默认使用的是父进程中的代码和数据(写时拷贝)。

💦 fork 的常规用法

如上代码,fork 之后与父进程执行一样的代码,有什么意义 ❓

  我直接让父进程做不就完了嘛,所以大部分情况下我们创建的父子进程,是想让父和子执行不同的代码。所以对我们而言,不是这样用 fork 的,而是通过 fork 的返回值来进行代码的分支功能。

在之前的学习中我们都知道 if … 、else if …,是不能同时进入的,那有没有可能它们能同时进入且跑 2 份死循环呢 ❓

  放在以前根本不可能,因为它是单进程,而现在我们使用 fork 创建父子进程(多进程),所以对于 if … 、else if …,它都会被进入,且 2 个死循环都会跑。对我们来讲这里的父进程就是自己,然后你自己 fork 创建了子进程,所以从 fork 之后,就有 2 个执行流,其中子进程执行 if,父进程执行 else if。

  运行后可以看到 fork 之前只有 1 个进程,但 fork 后就有 2 个进程一起运行,注意这里是系统来规定父子进程执行的先后顺序。这里肯定是并发,因为我的云服务器只是 1 核的配置,所以它底层其实是以 进程快速切换 来达到伪并行的效果。

💨小结:

  就意义而言,我们创建子进程是想帮助父进程来完成任务的,现在我们刚涉及,所以让它俩各自输出。如果我们要实现边下载边播放的功能那么价值就可以体现了,这样就可以实现一个并发执行的多进程程序。

ret == 0 && ret > 0 能同时存在吗 ❓

  按以前的知识,照现在看到的场景,用于接收 fork 返回值的 ret 是怎么可以既等于 0,又大于 0 的,在我们 C/C++ 上是绝对不可能的。这个的理解是需要我们进程控制中的 进程地址空间 的知识来铺垫才能理解的。

fork 为啥会有 2 个返回值 ❓

  我们在调用一个函数时,这个函数已经准备 return 了,那么就认为这个函数的功能完成了,return 并不属于这个函数的功能,而是告诉调用方我完成了,比如 fork 在准备 return 前,fork 创建子进程的工作已经完成了,甚至子进程已经被放在调度队列里了。我们刚刚说过,fork 之后,父子进程是共享代码的,我们认定 return 是代码,是和父子进程共享的代码,所以当我们父进程 return 时,这里的子进程也要 return,所以说这里的父子进程会使 fork 返回 2 个值。

为啥给子进程返回 0,而父进程返回子进程的 pid ❓

   在生活中,对于儿子,只有 1 个父亲,而对于父亲,却可以有多个儿子,比如家里有 3 个儿子,其中老二犯了错,父亲不可能说 “ 儿子,过来,我抽你一顿 ”,而应该是说 “ 老二 过来,我抽你一顿 ”;而儿子可以说 “ 爸爸,我来了 ”。可以看到父亲为了能更好的吩咐儿子,会对每个儿子进行标识,并且记住它们。所以父进程返回子进程的 pid 的原因是因为父进程可能会创建多个子进程(好比你出生后你爸就给你起了个名字),所以这为了保证父进程能拿到想拿到的子进程;而子进程返回 0 的原因是父进程对于子进程是唯一的(好比你不可能给你爸起名字)。

父进程拿子进程干嘛 ???

  那你爸拿你的名字干嘛,肯定是叫你办事呀,同样的父进程拿子进程有很多用途:比如说有 5 个子进程,我想把某个任务指派给某个子进程,这时就通过它的 pid 来指定;当然你要杀掉某个子进程,可以使用 pid 来杀掉想杀掉的子进程。

子进程的 pid 会存储在父进程的 PCB ???

  不会,因为子进程的 pid 是给你看的,你可以拿着 pid 去搞事情。而实际在内核里它们是由对应的链表结构去维护的。

如何创建多个子进程 ???

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
void DoThing()
{
  int count = 0;
  while(count < 5)
    {
        printf("pid : %d, ppid : %d, count : %d\n", getpid(), getppid(), count);
        count++;
        sleep(1);
    }
}
int main()
{
    pid_t ids[5];
    printf("I am father : %d\n", getpid());
    for(int i = 0; i < 5; i++)
    {
        ids[i] = fork();
        if(ids[i] == 0)
        {
            //child
            DoThing();
            exit(1);
        }
    }
    printf("%d %d %d %d %d\n", ids[0], ids[1], ids[2], ids[3], ids[4]);
    getchar();                                                                                                                                                                          
    return 0;
}

运行后:

💦 进程状态

1、Linux 内核源码

其中 task_state_array[] 里描述的是 Linux 的进程状态:

2、R (running)

进程是 R 状态,一定在 CPU 上运行 ❓

  进程在运行队列中,就叫做 R 状态,也就是说进程想被 CPU 运行,前提条件是你必须处于 R 状态,R :我准备好了,你可以调度我。

为啥我在跑,但状态却是 S ❓

  因为代码大部分时间是在 sleep 的,且每次 1 秒钟,其次 printf 是往显示器上输出的,涉及到 I/O,效率比较低,一定会要求进程去等我们把数据刷新到显示器上。所以综合考量,我们这个程序可能只有万分之一的时间在运行,其它时间都在休眠,站在用户的角度它是 R,但是对于操作系统来说它不一定是 R,它有可能在队列中等待调度。

如果我们就想看下 R 状态呢 ???

循环里啥都不要做。

3、S (sleeping)

休眠状态(浅度休眠,大部分情况)。这种休眠是可被换醒的,我们可以 Ctrl + C 退出循环,而此时的进程就没了,也就是说它虽然是一种休眠状态,但是它随时可以接收外部的信号,处理外部的请求。

4、D (disk sleep)

休眠状态(深度休眠)

  此时进程拿着一批数据找到了磁盘说:磁盘,你帮我把数据放在你对应的位置。磁盘说:好嘞,然后磁盘就慢慢地写到对应的位置。此时进程处于等待状态,它在等把数据写完,然后告诉进程写入成功 or 失败。此时操作系统过来说:你没发现现在内存严重不足了吗,我现在要释放一些闲置的内存资源,随后就把进程干掉了。磁盘写失败后,然后跟进程说:不好意思,我写失败了,然而进程已经挂了,此时我们的数据流向就不确定了。

对于上面的场景,这个锅由谁来背 —— 操作系统/内存/磁盘 ❓

于是它们三方开始了辩论:

  操作系统说,你在那等,我又不知道你在等啥,系统内存不足了,我就尽我的职责,我的工程师就是这样写我的,杀掉闲置的内存。假如我这次不杀你,那你说下次我再遇到一些该杀死的闲置的内存,我怕我又被责怪,所以没杀,你就认为我不作为 ?操作系统说:我又识别不了哪些进程是重要或不重要的。

  磁盘说,我就是一个跑腿的,你们让我干啥就干啥,又不是写入的结果不告诉你,而是你不在了。

  进程说,我在那规矩的等着呢,是有人把我杀了,我自己也不想退出。

这里好像谁也没有错,但是确实出现了问题,你难道说错的是用户 —— 内存买小了吗 ?无论是操作系统、内存、磁盘都是为了给用户提供更好的服务。根本原因是操作系统能杀掉此进程,如果让操作系统不能杀掉此进程就可以了。我现在做的事情很重要,即便操作系统再牛,也杀不了我,你系统内存不够了,你想其它办法去,不要来搞我。所以我们针对这种类型的进程我们给出了 D 状态,所以操作系统从此就知道了以后 D 是个大哥,不能搞。

所以对于深度睡眠的进程不可以被杀死,即便是操作系统。通常在访问磁盘这样的 I/O 设备,进行数据拷贝的关键步骤上,是需要将进程设置为 D 的,好比 1 秒钟内,平台有 100 万的用户注册,如果数据丢失,那么带来的损失是巨大的。

对于深度睡眠的进程怎么结束 ???

  只能等待 D 状态进程自动醒来,或者关机重启,但有可能会卡住。深度睡眠的进程我们没法演示,万一把自己的机器玩挂了,成本较高。

不管是浅度睡眠还是深度睡眠都是一种等待状态,因为某种条件不满足。

5、T (stopped)

对于一个正在运行的进程,怎么暂停 ❓

使用 kill -l 命令,查看信号,这里更多内容后面我们再学习:

使用 kill -19 27918 命令,给 27918 进程发送第 19 号信号来暂停进程:

使用 kill -18 27918 命令,给 27918 进程发送第 18 号信号来恢复进程:

我们也可以认为 T 是一种等待状态。

6、T (tracing stop)

  当你使用 vs of gdb 调试代码,比如你打了一个断点,然后开始调试,此时在断点处停下来的状态就是 t,这里是为了和上面进行区分。这里先不细谈。

7、Z (zomble)

  比如你早上去晨跑时,突然看到其他跑友躺地上,你虽然救不了人,也破不了案,但是作为一个热心市民,可以先给 120 打电话,再给 110 打电话。随后警察来了,第一时间肯定不会把这个人抬走,清理现场,如果是这样的话凶手肯定会笑开花,第一时间肯定是先确定人是正常死亡还是非正常死亡,如果是非正常死亡,那么立马封锁现场,拉上警戒线,判断是自杀的还是他杀。随后 120 来了,对人的状态进行判断,如果是正常死亡,就判断是因为疾病,还是年纪大了。最终判断出人是是因为疾病离开的,警察和医生的任务已经完成后,不会就把人放这,直接撤了。而是把人抬走,恢复地方秩序,然后通知家属。所以当一个人死亡时,并不是立马把这个人从世界上抹掉,而是分析这个人身上的退出信息,比如说体态特征、血压等信息来确定具体的退出原因。

  同样进程退出,一般不是立马让 OS 回收资源,释放进程所有的资源,作为一个死亡的进程,OS 不会说你已经死了,就赶紧把你拉到火葬场,而是 OS 要知道进程是因为什么原因退出的。创建进程的目的是为了完成某件任务,进程退出了,我得知道他把我任务完成的怎么样了,所以 OS 在进程退出时,要搜集进程退出的相关有效数据,并写进自己的 PCB 内,以供 OS 或父进程来进行读取。只有读取成功之后,该进程才算真正死亡,此时我们称该进程为 死亡状态 X,。其中我们把一个进程退出,但还没有被读取的那个时间点,我们称该进程为 僵尸状态 Z

  我作为父进程 fork 创建一个子进程,子进程死亡了,但父进程没通过接口让 OS 回收,此时子进程的状态就是 Z。

僵尸状态演示 ❓

这里我们可以写一个监控脚本 while :; do ps ajx | head -1 && ps ajx | grep mytest; sleep 1; echo "####################"; done 来观测:

8、X (dead)

  参考 Z 状态。其次 X 状态我们看不到,因为我们释放了,它是一瞬间的。

💦、补充说明

1、 S and S+

  一般在命令行上,如果是一个前台进程,那么它运行时状态后会跟 +。

也就是说前台进程一旦执行,bash 就无法进行命令行解释,ls、top 等命令都无法执行,只有 Ctrl + C 来进行终止:

如果想把一个进程放在后台可以 ./mytest &

其中对于后台进程,bash 可以对命令行解释:

但是你会发现 Ctrl + C 无法终止后台进程,只能对该进程发送第 9 号信号来结束进程:

2、 OS 描述的状态 and 具体的 Linux 进程状态

其中新建没有对应的 Linux 进程状态;就绪可对应到 Linux 进程中的 R;运行也可对应到 Linux 进程中的中的 R;退出可对应到 Linux 进程中的 Z/X;阻塞可对应到 Linux 进程中的 S/D/T;

所以 Linux 状态的实现和操作系统的实现是有点差别的。操作系统的所描述的概念是所有操作系统都遵守这样的规则,而 Linux 就是一种具体的操作系统规则。

3、僵尸进程的危害
  1. 进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于 Z 状态。
  2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 task_struct(PCB) 中,换句话说,Z 状态一直不退出,PCB 一直都要维护。
  3. 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,想想 C 中定义一个结构体变量(对象),就是要在内存的某个位置进行开辟空间。
  4. 内存泄漏。
  5. 如何避免,后面再谈。
4、孤儿进程

父进程如果提前退出,那么子进程就是 孤儿进程,那么子进程退出,进入 Z 之后,该怎么处理 ❓

可以看到 5 秒前有 2 个进程,5 秒后父进程死亡了(这里没有被僵尸的原因是父进程也有父进程 23178 -> bash),只有 1 个子进程。这里我们称没有父进程的子进程为孤儿进程,此时孤儿进程会被 1号进程领养,它是 systemd(操作系统),此时操作系统就可以直接对我回收资源。 且进程状态会由前台转换为后台,后台进程可以使用 第 9 号信号来结束进程。

5、1 号进程

操作系统启动之前是有 0号进程的,只不过完全启动成功后,0号进程就被1号进程取代了,具体的取代方案,后面学习 进程替换时再谈。可以看到 pid 排名靠前的进程都是由 root 来启动的。注意在 Centos7.6 下,它的 1号进程 叫做systemd,而 Centos6.5 下,它的 1号进程 叫做initd

四、Linux 系统中的优先级

💦 基本概念

  这里我们已经在上面说过了,就不谈了。

💦 PRI and NI

通过ps -al查看当前进程相关信息:

  1. PRI 比较好理解,即进程的优先级,或者通俗点说就是程序被 CPU 执行的先后顺序,此值越小,进程的优先级别越高。
  2. NI 就是我们所要说的 nice 值了,其表示进程可被执行的优先级的修正数值。
  3. PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为:PRI(new) = PRI(old) + nice,这里的 old 永远是 80,下面解释。
  4. 当 nice 值为负值的时候,那么该程序优先级值将变小,其优先级会变高,则越快被执行;当 nice 值为正值时,意味着你要调低优先级。所以调整进程优先级,在 Linux 下,就是调整进程 nice 值。
  5. nice 其取值范围是闭区间 -20 至 19,一共 40 个级别 —— -20 至 0,0 至 19。
  6. PID 是当前进程的专属标识;PPID 是当前进程的父进程的专属标识;TTY 可以理解为终端设备;CMD 是当前进程的命令。
  7. UID 是执行者的身份。
    ll后,其中可以看到我:

    ll -n,就可以看到我的 ID:
    也就是说在 Linux 中标识一个用户,并不是通过用户名,而是通过用户的 UID。比如 qq 里,每人都有一个昵称,如果昵称可以随便改的话,就意味着昵称不是标识你的唯一方式,而是通过 qq 号码来唯一标识你。所以对于操作系统来说,当你新建用户时,除了你自己给自己起的名称之外,还有操作系统所分配给你的 UID。原因是因为计算机比较擅于处理数据

    所以可以看到这里的进程是我启动的:

饥饿问题 ❓

  Linux 中的优先级由prinice值共同确定。Linux 优先级的特点,对于普通进程,优先级的数值越小,优先级越高;优先级的数值越大,优先级越低。但是优先级不可能一味的高,也不可能一味的低,比如说优先级最高的是 30,最低的是 99,那么我们不可以把最高搞成 -300,最低搞成 999。为啥优先级能设置,但不能很夸张的设置,是因为即使再怎么优先,操作系统的调度器也要适度的考虑公平问题,比如我把 A进程优先级搞到 -300,对我来讲,A进程老是得到资源,别人长时间得不到资源,这种就叫饥饿问题。好比你在打饭窗口排着队呢,老是有些人觉得自己优先级高往前插队,那么你就长时间打不到饭,导致最后吃不到饭。所以 CPU 也是有度的来根据优先级调度。

其中 pri 的优先级是多少就是多少,但实际上 Linux 的优先级是可以被修正的,nice 值就是优先级的修正数据 [-20 ~ 19],其中 -20 优先级最高,19 优先级最低。

💦 PRI vs NI

  1. 需要强调一点的是,进程的 nice 值不是进程的优先级,他们不是一个概念,但是进程 nice 值会影响到进程的优先级变化。
  2. 可以理解 nice 值是进程优先级的修正数据。

💦 调整优先级

ps -al查看当前进程优先级:

top命令查看所有进程相关信息:

r命令后输入要调整的 pid:

给 27598 进程 Renice 要调整的 nice 值:

ps -al验证:

继续调整时,它不让我调了:

sudo top提升权限进行调整:

ps -al验证:

之前第一次调整后的优先级是 93,随后第二次调整后的优先级应该是 103,但是却是 90 ❓

  其中我们在 Linux 中进行优先级调整时,pri 永远是默认的 80,也就是说即使你曾经调整过 nice 值,当你再次调整 nice 值时,你的优先级依旧是从 80 开始的,也就是说PRI(new) = PRI(old) + nice 中的 old 永远是 80,这个现象很奇怪哈,我们继续往下走。

上面说每次调整优先级永远是从 80 开始,上面又说 nice 值的最小值是 -20,意味着 nice 值是 -100,不会真正的设置到 -100,而是设置成了 nice 值的最小值 -20:

ps -al验证:

我们发现最小的 nice 值就是 -20,而它的优先级最高只能到 60。

继续往下走,瞅瞅它的优先级最低是多少:

所以此时调整后的优先级是 99。

ps -al验证:

我们发现最大的 nice 值就是 19,而它的优先级最高只能到 99。

每次我们重新计算新的优先级时, old 为啥默认都是 80 ❓

  其一,有一个基准值,方便调整。你都想调整了,意味着你不想要老的优先级,那么我给你一个基准点,下次就方便许多了,否则你每次调整之前,还得先查一下当前进程现在的优先级。

  其二,大佬并不想让我对对一个进程的优先级设置的很高或很低,old 每次都是 80,同时 nice 值区间是 [-20, 19],最终你的优先级区间 [60, 99],这样的设计,成本不高。

nice 值是 [-20, 19],意味着当前的 nice 值是一种可控状态,为啥 ❓

  也就意味着这个值,你可以往大了设置,也可以往小了设置。进程是被操作系统调度的,如果可以让一个用户按他的需求去定制当前进程的优先级,比如我把我的进程优先级搞成 1,其它进程优先级搞成 10000,那么这样调度器就没有公平可言了。所以本质是操作系统中的调度器要公平较高效的调度,这是基本原则。

调度器的公平 ???

  这里不是指平均。有多个进程,不是说我现在给你调度了 5 毫秒,就一定要给其它进程都调度 5 毫秒。而必须得结合当前进程的特性去进行公平调度的算法。所以这里的公平可以理解为我们是尽量的给每个进程尽快、尽好的调度,尽量不落下任何一个进程,但并不保证我们在同一时间上启动的所有进程在调度时间上完全一样,而只能说是大致一样,因为调度器是服务计算机上所有进程的。

【写在后面】

  1. 可以看到 Linux 它的进程状态,一会僵尸,一会孤儿,感觉 Linux 操作系统很惨的样子。实际上后面我们还会再学一种守护进程(精灵进程)
  2. 如果一个进程是 D 状态是不能 kill -9 的;但如果一个进程是 Z 状态,那么它能 kill -9 吗 ❓
      如果一个人已经死了,你上去踢它两脚,有用吗 ?所以一个进程是 Z 状态,你去 kill 它是杀不掉的。
  3. [ 面试题 ]:什么 样的进程杀不死 ❓
      D 状态进程 and Z 状态进程。因为一个是在深度休眠,操作系统都得叫大哥,一个是已经死了。
  4. 并行:多个进程在多个 CPU 下分别,同时运行,这称之为并行。
  5. 并发:多个进程在一个 CPU 下采用进程切换的方式,在一段时间内,让多个进程都得以推进,这称之为并发。
  6. 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。独立性也是操作系统设计进程的一个原则,不管你是 Linux、Windows、Macos、Android 都需要遵守,代码共享,数据各自私有就是为了实现独立性原则。

    此时可以看到子进程 5 秒后报错退出后并不会影响父进程:

  7. 竞争性:系统进程数目众多,而 CPU 资源少量,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。你现在为什么正在看我的文章呢,根本原因就是因为社会大环境里需要竞争,而你需要提升自己的竞争力和优先级。


相关文章
|
11天前
|
安全 算法 Unix
深入浅出操作系统:从基础概念到实践应用
【10月更文挑战第22天】本文旨在以浅显易懂的语言,为读者揭开操作系统的神秘面纱。我们将从操作系统的基本概念出发,逐步深入其核心功能与设计哲学,并通过具体代码示例,展示操作系统如何在实际中发挥作用。无论你是计算机科学的学生,还是对技术有浓厚兴趣的爱好者,这篇文章都将为你提供一次轻松愉快的操作系统之旅。
26 4
|
1月前
|
Ubuntu Java Linux
Linux操作系统——概念扫盲I
Linux操作系统——概念扫盲I
40 4
|
2月前
|
存储 算法 安全
深入理解操作系统:从基础概念到代码实践
【9月更文挑战第23天】本文将带领读者深入探索操作系统的奥秘,从基础概念出发,逐步揭示操作系统的工作原理和设计哲学。我们将通过实际代码示例,展示操作系统如何与硬件交互、管理资源以及提供用户界面。无论你是计算机专业的学生还是对操作系统感兴趣的开发者,这篇文章都将为你打开一扇通往操作系统世界的大门。
67 16
|
2月前
|
数据采集 消息中间件 并行计算
进程、线程与协程:并发执行的三种重要概念与应用
进程、线程与协程:并发执行的三种重要概念与应用
56 0
|
3月前
|
Linux 调度 芯片
Linux0.11 内核体系结构(八)(下)
Linux0.11 内核体系结构(八)
56 1
|
3月前
|
存储 缓存 Linux
在Linux中,文件系统概念是什么?
在Linux中,文件系统概念是什么?
|
3月前
|
Linux Shell 编译器
Linux0.11 内核体系结构(八)(上)
Linux0.11 内核体系结构(八)
105 0
|
3月前
|
网络协议 安全 Linux
在Linux中,体系结构是什么?
在Linux中,体系结构是什么?
|
3月前
|
存储 安全 Linux
在Linux中,用户和组的概念是什么?
在Linux中,用户和组的概念是什么?
|
3月前
|
Linux 持续交付 虚拟化
在Linux中,Docker和容器虚拟概念是什么?
在Linux中,Docker和容器虚拟概念是什么?