解析并执行 shell 命令

e603e57d9b1357e56a84e279c1cc7aa3.gif

作者 | 闪客

来源 | 低并发编程

新建一个非常简单的 info.txt 文件。

name:flash
age:28
language:java

在命令行输入一条十分简单的命令。

[root@linux0.11] cat info.txt | wc -l
3

这条命令的意思是读取刚刚的 info.txt 文件,输出它的行数。 

在上一回中,我们讲述了进程在读取你的命令字符串时,可能经历的进程的阻塞与唤醒,也即 Linux 0.11 中的 sleep_on 与 wake_up 函数。

228a7c881112994078ca32bd90dccd1d.png

接下来,shell 程序就该解析并执行这条命令了。

// xv6-public sh.c
int main(void) {static char buf[100];// 读取命令while(getcmd(buf, sizeof(buf)) >= 0){// 创建新进程if(fork() == 0)// 执行命令runcmd(parsecmd(buf));// 等待进程退出wait();}
}

也就是上述函数中的 runcmd 命令。

首先 parsecmd 函数会将读取到 buf 的字符串命令做解析,生成一个 cmd 结构的变量,传入 runcmd 函数中。

// xv6-public sh.c
void runcmd(struct cmd *cmd) {...switch(cmd->type) {...case EXEC:ecmd = (struct execcmd*)cmd;...exec(ecmd->argv[0], ecmd->argv);... break;case REDIR: ...case LIST: ...case PIPE: ...case BACK: ...}
}

然后就如上述代码所示,根据 cmd 的 type 字段,来判断应该如何执行这个命令。

比如最简单的,就是直接执行,也即 EXEC

如果命令中有分号 ; 说明是多条命令的组合,那么就当作 LIST 拆分成多条命令依次执行。

如果命令中有竖线 | 说明是管道命令,那么就当作 PIPE 拆分成两个并发的命令,同时通过管道串联起输入端和输出端,来执行。

我们这个命令,很显然就是个管道命令。

[root@linux0.11] cat info.txt | wc -l

管道理解起来非常简单,但是实现细节却是略微复杂。

所谓管道,也就是上述命令中的 |,实现的就是将 | 左边的程序的输出(stdout)作为 | 右边的程序的输入(stdin),就这么简单。

那我们看看,它是如何实现的,我们走到 runcmd 方法中的 PIPE 这个分支里,也就是当解析出输入的命令是一个管道命令时,所应该做的处理。

// xv6-public sh.c
void runcmd(struct cmd *cmd) {...int p[2];...case PIPE:pcmd = (struct pipecmd*)cmd;pipe(p);if(fork() == 0) {close(1);dup(p[1]);close(p[0]);close(p[1]);runcmd(pcmd->left);}if(fork() == 0) {close(0);dup(p[0]);close(p[0]);close(p[1]);runcmd(pcmd->right);}close(p[0]);close(p[1]);wait(0);wait(0);break;...
}

首先,我们构造了一个大小为 2 的数组 p,然后作为 pipe 的参数传了进去。

这个 pipe 函数,最终会调用到系统调用的 sys_pipe,我们先不看代码,通过 man page 查看 pipe 的用法与说明。

1a23260e1a26e615582593035ea729fe.png

可以看到,pipe 就是创建一个管道,将传入数组 p 的 p[0] 指向这个管道的读口,p[1] 指向这个管道的写口,画图就是这样子的。

45b2c9439050a47377c2028b000cea98.png

当然,这个管道的本质是一个文件,但是是属于管道类型的文件,所以它的本质的本质实际上是一块内存

这块内存被当作管道文件对上层提供了像访问文件一样的读写接口,只不过其中一个进程只能读,另一个进程只能写,所以再次抽象一下就像一个管道一样,数据从一端流向了另一段。

你说它是内存也行,说它是文件也行,说它是管道也行,看你抽象到那一层了,这个之后再展开细讲,先让你迷糊迷糊。

回过头看程序。

// xv6-public sh.c
void runcmd(struct cmd *cmd) {...int p[2];...case PIPE:pcmd = (struct pipecmd*)cmd;pipe(p);if(fork() == 0) {close(1);dup(p[1]);close(p[0]);close(p[1]);runcmd(pcmd->left);}if(fork() == 0) {close(0);dup(p[0]);close(p[0]);close(p[1]);runcmd(pcmd->right);}close(p[0]);close(p[1]);wait(0);wait(0);break;...
}

在调用完 pipe 搞出了这样一个管道并绑定了 p[0] 和 p[1] 之后,又分别通过 fork 创建了两个进程,其中第一个进程执行了管道左边的程序,第二个进程执行了管道右边的程序

由于 fork 出的子进程会原封不动复制父进程打开的文件描述符,所以目前的状况如下图所示。

f543a36ac3b04a2f2e57563e077b3ae9.png

当然,由于每个进程,一开始都打开了 0 号标准输入文件描述符,1 号标准输出文件描述符和 2 号标准错误输出文件描述符,所以目前把文件描述符都展开就是这个样子。(父进程的我就省略了)

f20332d8d69304d6ccbe4d0d0fc7ba92.png

现在这个线条很乱,不过没关系,看代码。左边进程随后进行了如下操作。

// fs/pipe.c
...
if(fork() == 0) {close(1);dup(p[1]);close(p[0]);close(p[1]);runcmd(pcmd->left);
}
...

关闭(close)了 1 号标准输出文件描述符,复制(dup)了 p[1] 并填充在了 1 号文件描述符上(因为刚刚关闭后空缺出来了),然后又把 p[0] 和 p[1] 都关闭(close)了。

你再读读这段话,最终的效果就是,将 1 号文件描述符,也就是标准输出,指向了 p[1] 管道的写口,也就是 p[1] 原来所指向的地方。

31d0a147aa35c427faa8d65f34cacf5e.png

同理,右边进程也进行了类似的操作。

// fs/pipe.c
...
if(fork() == 0) {close(0);dup(p[0]);close(p[0]);close(p[1]);runcmd(pcmd->right);
}
...

只不过,最终是将 0 号标准输入指向了管道的读口。

12ee88b2c2dcbc896c5bfbd441a05105.png

这是两个子进程的操作,再看父进程。

// xv6-public sh.c
void runcmd(struct cmd *cmd) {...pipe(p);if(fork() == 0) {...}if(fork() == 0) {...}// 父进程close(p[0]);close(p[1]);...
}

你没有看错,父进程仅仅是将 p[0] 和 p[1] 都关闭掉了,也就是说,父进程执行的 pipe,仅仅是为两个子进程申请的文件描述符,对于自己来说并没有用处。

那么我们忽略父进程,最终,其实就是创建了两个进程,左边的进程的标准输出指向了管道(写),右边的进程的标准输入指向了同一个管道(读),看起来就是下面的样子。

a032c4d9132479c8716da2c08ad7cc6a.png

而管道的本质就是一个文件,只不过是管道类型的文件,再本质就是一块内存。所以这一顿操作,其实就是把两个进程的文件描述符,指向了一个文件罢了,就这么点事情。

那么此时,再让我们看看 sys_pipe 函数的细节。

// fs/pipe.c
int sys_pipe(unsigned long * fildes) {struct m_inode * inode;struct file * f[2];int fd[2];for(int i=0,j=0; j<2 && i<NR_FILE; i++)if (!file_table[i].f_count)(f[j++]=i+file_table)->f_count++;...for(int i=0,j=0; j<2 && i<NR_OPEN; i++)if (!current->filp[i]) {current->filp[ fd[j]=i ] = f[j];j++;}...if (!(inode=get_pipe_inode())) {current->filp[fd[0]] = current->filp[fd[1]] = NULL;f[0]->f_count = f[1]->f_count = 0;return -1;}f[0]->f_inode = f[1]->f_inode = inode;f[0]->f_pos = f[1]->f_pos = 0;f[0]->f_mode = 1;       /* read */f[1]->f_mode = 2;       /* write */put_fs_long(fd[0],0+fildes);put_fs_long(fd[1],1+fildes);return 0;
}

不出我们所料,和进程打开一个文件的步骤是差不多的,下图是进程打开一个文件时的步骤。

04cca89415d2acd255a140b6109976ff.png

而 pipe 方法与之相同的是,都是从进程中的文件描述符表 filp 数组和系统的文件系统表 file_table 数组中寻找空闲项并绑定。

不同的是,打开一个文件的前提是文件已经存在了,根据文件名找到这个文件,并提取出它的 inode 信息,填充好 file 数据。

而 pipe 方法中并不是打开一个已存在的文件,而是创建一个新的管道类型的文件,具体是通过 get_pipe_inode 方法,返回一个 inode 结构。然后,填充了两个 file 结构的数据,都指向了这个 inode,其中一个的 f_mode 为 1 也就是写,另一个是 2 也就是读。(f_mode 为文件的操作模式属性,也就是 RW 位的值)

创建管道的方法 get_pipe_inode 方法如下。

// fs.h
#define PIPE_HEAD(inode) ((inode).i_zone[0])
#define PIPE_TAIL(inode) ((inode).i_zone[1])// inode.c
struct m_inode * get_pipe_inode(void) {struct m_inode *inode = get_empty_inode();inode->i_size=get_free_page();inode->i_count = 2; /* sum of readers/writers */PIPE_HEAD(*inode) = PIPE_TAIL(*inode) = 0;inode->i_pipe = 1;return inode;
}

可以看出,正常文件的 inode 中的 i_size 表示文件大小,而管道类型文件的 i_size 表示供管道使用的这一页内存的起始地址。

OK,管道的原理在这里就说完了,最终我们就是实现了一个进程的输出指向了另一个进程的输入。

8494c2a694d52e00efa3a77835ebab0f.png

回到最开始的 runcmd 方法。

// xv6-public sh.c
void runcmd(struct cmd *cmd) {...switch(cmd->type) {...case EXEC:ecmd = (struct execcmd*)cmd;...exec(ecmd->argv[0], ecmd->argv);... break;case REDIR: ...case LIST: ...case PIPE: ...case BACK: ...}
}

如果展开每个 switch 分支你会发现,不论是更换当前目录的 REDIR 也就是 cd 命令,还是用分号分隔开的 LIST 命令,还是我们上面讲到的 PIPE 命令,最终都会被拆解成一个个可以被解析为 EXEC 类型的命令。

EXEC 类型会执行到 exec 这个方法,在 Linux 0.11 中,最终会通过系统调用执行到 sys_execve 方法。

这个方法就是最终加载并执行具体程序的过程,我们已经讲过如何通过 execve 加载并执行 shell 程序了,并且在加载 shell 程序时,并不会立即将磁盘中的数据加载到内存,而是会在真正执行 shell 程序时,引发缺页中断,从而按需将磁盘中的数据加载到内存。

这个流程在本回我们就不再赘述了,不过当初在讲这块流程以及其它需要将数据从硬盘加载到内存的逻辑时,总是跳过这一步的细节。

那么我们下一回,就彻底把这个硬盘到内存的流程拆开了讲解!

欲知后事如何,且听下回分解。

b3a8d0679d5315e4f9d2aa0b8f8eed9b.gif

往期推荐

Docker 那些事儿:如何安全地停止、删除容器?

剖析 kubernetes 集群内部 DNS 解析原理

Kubernetes 在科技革命中的演变

实战 Kubectl 创建 Deployment 部署应用

97743af54f4e34290b93de5032cb383c.gif

点分享

b4ea69de6c01ef2d9ab36b24f1646914.gif

点收藏

4b7f0f806547513f6884c8b8695b9337.gif

点点赞

793c55e3d45ae9b9f21989615ad23dc0.gif

点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511251.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

EventBridge消息路由|高效构建消息路由能力

简介&#xff1a;企业数字化转型过程中&#xff0c;天然会遇到消息路由&#xff0c;异地多活&#xff0c;协议适配&#xff0c;消息备份等场景。本篇主要通过 EventBridge 消息路由的应用场景和应用实验介绍&#xff0c;帮助大家了解如何通过 EventBridge 的消息路由高效构建消…

哪吒汽车选择BlackBerry QNX为中国新能源轿跑——哪吒S保驾护航

BlackBerry与合众新能源汽车有限公司近日宣布达成合作&#xff0c;合众汽车旗下汽车品牌——中国造车新势力哪吒汽车&#xff0c;在其即将量产的运动型智享轿跑——哪吒S中搭载了BlackBerry QNX为其保驾护航&#xff0c;旨在确保关键系统的功能安全、网络安全与可靠性的同时&am…

异步请求积压可视化|如何 1 分钟内快速定位函数计算积压问题

简介&#xff1a;本文分为三个部分&#xff1a;概述中引入了积压问题&#xff0c;并介绍了函数计算异步调用基本链路&#xff1b;并在指标介绍部分详细介绍了指标查看方式&#xff0c;分类解读了不同的指标含义&#xff1b;最后以一个常见的异步请求积压场景为例&#xff0c;介…

并发-分布式锁质量保障总结

简介&#xff1a;并发问题是电商系统最常见的问题之一&#xff0c;例如库存超卖、抽奖多发、券多发放、积分多发少发等场景&#xff1b;之所以会出现上述问题&#xff0c;是因为存在多机器多请求同时对同一个共享资源进行修改&#xff0c;如果不加以限制&#xff0c;将导致数据…

以网强算,中国移动算网建设激发澎湃能量

近日&#xff0c;在首届中国算力大会上&#xff0c;中国工程院院士张宏科发表演讲认为&#xff0c;“信息网络已经成为大国博弈的核心与关键&#xff0c;面临机遇期&#xff0c;我们亟需新型网络体系与技术创新&#xff0c;满足自主可控和建设网络强国的重大战略需求&#xff0…

云上的移动性能测试平台

简介&#xff1a;功能决定现在&#xff0c;性能决定未来。欢迎大家围观《云上的移动性能测试平台》&#xff0c; 了解EMAS性能测试平台的能力与规划。 1. 功能决定现在&#xff0c;性能决定未来 性能测试在移动测试领域一直是一个大难题&#xff0c;它最直观的表现是用户在前…

Docker 镜像和容器的导入导出及常用命令

作者 | 微枫Micromaple来源 | CSDN博客Docker 镜像和容器的导入导出1.1 镜像的导入导出1.1.1 镜像的保存通过镜像ID保存方式一&#xff1a;docker save image_id > image-save.tar例如&#xff1a;rootUbuntu:/usr/local/docker/nginx# docker imagesREPOSITORY TAG …

阿里云「低代码音视频工厂」正式上线,为企业用户打造音视频应用开发最短路径

简介&#xff1a;vPaaS全新定义企业级音视频应用开发 1月5日&#xff0c;阿里云视频云“低代码音视频工厂vPaaS“正式上线&#xff0c;极大程度降低音视频开发门槛&#xff0c;打破传统音视频技术壁垒&#xff0c;全新定义企业级的音视频应用开发。 低代码音视频工厂基于云原生…

数组方法 包含es6

有回调函数的方法都有两个参数&#xff08;不写默认是window) 例&#xff1a;map&#xff0c;forEach&#xff0c;find let arr[1,2,3,4]; let obj{a:1,b:2}; let _thisnull; arr.map(v>{_thisthisreturn v1 },obj) console.log(_this) 数组方法细则 方法功能参数返回值是…

阿里开源支持10万亿模型的自研分布式训练框架EPL(EasyParallelLibrary)

简介&#xff1a;EPL背后的技术框架是如何设计的&#xff1f;开发者可以怎么使用EPL&#xff1f;EPL未来有哪些规划&#xff1f;今天一起来深入了解。 作者 | 王林、飒洋 来源 | 阿里技术公众号 一 导读 最近阿里云机器学习PAI平台和达摩院智能计算实验室一起发布“低碳版”巨…

如何从 Docker 镜像里提取 dockerfile!

作者 | A-刘晨阳来源 | CSDN博客今天给大家分享一下 dockerfile 里面是如何写的&#xff0c;然后去查了查就有了新的发现——通过镜像来提取 dockerfile。从镜像中提取dockerfile的两种方法。history参数我们可以直接用docker自带的参数来查看镜像的dockerfile&#xff0c;但有…

新品发布|备案变更不用再担心中断服务啦

简介&#xff1a;ICP备案迁移服务&#xff0c;就是面向有计划变更域名备案主体&#xff0c;或者在不同主体间迁移网站备案信息的客户&#xff0c;实现迁移过程中域名或者网站服务不中断的备案增值服务。 说起ICP备案&#xff0c;做过互联网经营业务的朋友都很熟悉&#xff0c;…

一眼定位问题,函数计算发布日志关键词秒检索功能

简介&#xff1a;当 FaaS 应用出现很多报错&#xff0c;且调用日志页面的请求过多时&#xff0c;如何才能简单、快速地查到出现 bug 的原因&#xff1f; 听说这个问题你也遇到了&#xff1f; 小王是一名程序员&#xff0c;最近在使用 FaaS &#xff08…

如何在 Linux 中使用 rsync 传输文件

作者 | 刘光录来源 | TIAPrsync&#xff08;远程同步&#xff0c;Remote Sync&#xff09;是一种在系统中或两个系统之间复制文件和目录的同步工具。rsync 的一个最大的优点&#xff0c;就是它只复制更改的文件&#xff0c;因而可以减少 CPU 消耗&#xff0c;在复制文件时节省带…

国民级消消乐背后的网络技术支持:不畏巨“峰”,“运”筹帷幄

简介&#xff1a;近日&#xff0c;阿里云网络携手乐元素共同部署建设了基于7层业务自动化调度的弹性网络架构&#xff0c;进一步提升乐元素在用户服务上的娱乐体验。提到乐元素相信大家都不陌生&#xff0c;作为从事移动网络游戏的研发、运营及广告平台&#xff0c;其代表作就是…

透析阿里云视频云「低代码音视频工厂」之能量引擎——vPaaS视频原生应用开发平台

简介&#xff1a;支撑15分钟上线高品质专属音视频平台 为满足企业用户极速搭建高品质专属音视频业务的需求&#xff0c;阿里云视频云的“低代码音视频工厂”应运而生&#xff0c;但极速而高品质的平台搭建诉求&#xff0c;需要用全新的开发方式才能真正实现&#xff0c;而全新…

自动驾驶“稳打地基”,小鹏汽车基于阿里云建自动驾驶AI智算中心算力可达600PFLOPS

数据驱动是自动驾驶发展的公认方向&#xff0c;也让自动驾驶模型训练成为一头“吃算力”的巨兽。自动驾驶的视觉检测、轨迹预测与行车规划等算法模型&#xff0c;有赖于机器学习海量数据集&#xff0c;但算力的不足让研发速度仍远远赶不上数据量增长的速度。随着传感器的进一步…

阿里云视频云「 vPaaS 」演绎了怎样的音视频应用开发「未来图景」

简介&#xff1a;前瞻音视频平台的演进未来 vPaaS是阿里云视频云最新推出的低代码音视频应用开发产品&#xff0c;其中&#xff0c;vPaaS低代码音视频工厂&#xff0c;彻底打破了音视频应用的繁冗技术开发壁垒&#xff1b;vPaaS视频原生应用开发平台&#xff0c;全新定义了音视…

鲲鹏开发者创享日2022:鲲鹏全栈创新 与开发者共建数字湖南

由华为推出的面向鲲鹏计算产业全栈开发者的系列活动——鲲鹏开发者创享日2022于8月5日在长沙成功举办。这场被称为开发者“技术嘉年华”的峰会&#xff0c;汇聚了国内顶尖技术大咖、科研带头人、知名企业技术专家及高校开发者&#xff0c;描绘了计算产业发展趋势和蓝图&#xf…

MySQL 深潜 - MDL 锁的实现与获取机制

简介&#xff1a;本文将介绍在 MDL 系统中常用的数据结构及含义&#xff0c;然后从实现角度讨论 MDL 的获取机制与死锁检测&#xff0c;最后分享在实践中如何监控 MDL 状态。 作者 | 泊歌 来源 | 阿里技术公众号 一 背景 为了满足数据库在并发请求下的事务隔离性和一致性要求…