关于 Unix 命令 `yes` 的小故事

原文阅读:A Little Story About the `yes` Unix Command

写在前面:瑟瑟发抖的首次翻译

这是第一次动手翻译一篇外文,看懂和翻懂是不一样的,你所见到的是 v3.0 版本…

感谢 @依云 信雅达的科普和满满的批注,还有依云和 @Tranch 传奇老师的最后的校正,以及,@Honwhy 的文章分享~

如果你发现本文有任何一处翻译不当的,欢迎指教,感谢感谢(///▽///)

译文开始

你所知的最简单的 Unix 命令是什么呢?

echo命令,用于将字符串打印到标准输出流,并以 o 为结束的命令。

在成堆的简单 Unix 命令中,也有 yes 命令。如果你不带参数地运行yes命令,你会得到一串无尽的被换行符分隔开的 y 字符流:

y
y
y
y
(...你明白了吧)

一开始看似无意义的东西原来它是非常的有用:

yes | sh 糟心的安装.sh

你曾经有安装一个程序,需要你输入“y”并按下回车继续安装的经历吗?yes命令就是你的救星。它会很好地履行安装程序继续执行的义务,而你可以继续观看 Pootie Tang.(一部歌舞喜剧)。

编写 yes

emmm,这是 BASIC 编写 ‘yes’的一个基础版本:

10 PRINT "y"
20 GOTO 10

下面这个是用 Python 实现的编写 ‘yes’:

while True:
    print("y")

看似很简单?不,执行速度没那么快!
事实证明,这个程序执行的速度非常慢。

python yes.py | pv -r > /dev/null
[4.17MiB/s]

和我 Mac 自带的版本执行速度相比:

yes | pv -r > /dev/null
[34.2MiB/s]

所以我重新写了一个执行速度更快的的 Rust 版本,这是我的第一次尝试:

use std::env;

fn main() {
  let expletive = env::args().nth(1).unwrap_or("y".into());
  loop {
    println!("{}",expletive);
  }
}

解释一下:

  • 循环里想打印的那个被叫做expletive字符串是第一个命令行的参数。expletive这个词是我在yes书册里学会的;
  • unwrap_orexpletive传参,为了防止参数没有初始化,我们将yes作为默认值
  • into()方法将默认参数将从单个字符串转换为堆上的字符串

来,我们测试下效果:

cargo run --release | pv -r > /dev/null
   Compiling yes v0.1.0
    Finished release [optimized] target(s) in 1.0 secs
     Running `target/release/yes`
[2.35MiB/s]

emmm,速度上看上去并没有多大提升,它甚至比 Python 版本的运行速度更慢。这结果让我意外,于是我决定分析下用 C 实现的写入‘yes’程序的源代码。

这是 C 语言的第一个版本 ,这是 Ken Thompson 在 1979 年 1 月 10 日 Unix 第七版里的 C 实现的编写‘yes’程序:

main(argc,argv)
char **argv;
{
  for (;;)
    printf("%s\n",argc>1? argv[1]: "y");
}

这里没有魔法。

将它同 GitHub 上镜像的 GNU coreutils 的 128 行代码版 相比较,即使 25 年过去了,它依旧在发展更新。上一次的代码变动是在一年前,现在它执行速度快多啦:

# brew install coreutils
gyes | pv -r > /dev/null 
[854MiB/s]

最后,重头戏来了:

/* Repeatedly output the buffer until there is a write error; then fail.  */
while (full_write (STDOUT_FILENO,buf,bufused) == bufused)
  continue;

wow,让写入速度更快他们只是用了一个缓冲区。 常量BUFSIZ用来表明这个缓冲区的大小,根据不同的操作系统会选择不同的缓冲区大小【写入/读取】操作高效(延伸阅读传送门 。我的系统的缓冲区大小是 1024 个字节,事实上,我用 8192 个字节能更高效。

好,来看看我改进的 Rust 新版本:

use std::io::{self,Write};

const BUFSIZE: usize = 8192;

fn main() {
  let expletive = env::args().nth(1).unwrap_or("y".into());
  let mut writer = BufWriter::with_capacity(BUFSIZE,io::stdout());
  loop {
    writeln!(writer,"{}",expletive).unwrap();
  }
}

最关键的一点是,缓冲区的大小要是 4 的倍数以确保内存对齐

现在运行速度是 51.3MiB/s ,比我系统默认的版本执行速度快多了,但仍然比 Ken Thompson 在 [高效的输入输出] (https://www.gnu.org/software/... 文中说的 10.2GiB/s 慢。

更新

再一次,Rust 社区没让我失望。

这篇文章刚发布到 Reddit 的 Rust 板块, Reddit 的用户 nwydo 就提到了之前关于速率问题的讨论 。这个是先前讨论人员的优化代码,它打破了我机子的 3GB/s 的速度:

use std::env;
use std::io::{self,Write};
use std::process;
use std::borrow::Cow;

use std::ffi::OsString;
pub const BUFFER_CAPACITY: usize = 64 * 1024;

pub fn to_bytes(os_str: OsString) -> Vec<u8> {
  use std::os::unix::ffi::OsStringExt;
  os_str.into_vec()
}

fn fill_up_buffer<'a>(buffer: &'a mut [u8],output: &'a [u8]) -> &'a [u8] {
  if output.len() > buffer.len() / 2 {
    return output;
  }

  let mut buffer_size = output.len();
  buffer[..buffer_size].clone_from_slice(output);

  while buffer_size < buffer.len() / 2 {
    let (left,right) = buffer.split_at_mut(buffer_size);
    right[..buffer_size].clone_from_slice(left);
    buffer_size *= 2;
  }

  &buffer[..buffer_size]
}

fn write(output: &[u8]) {
  let stdout = io::stdout();
  let mut locked = stdout.lock();
  let mut buffer = [0u8; BUFFER_CAPACITY];

  let filled = fill_up_buffer(&mut buffer,output);
  while locked.write_all(filled).is_ok() {}
}

fn main() {
  write(&env::args_os().nth(1).map(to_bytes).map_or(
    Cow::Borrowed(
      &b"y\n"[..],),|mut arg| {
      arg.push(b'\n');
      Cow::Owned(arg)
    },));
  process::exit(1);
}

一个新的实现方式!

  • 我们预先准备了一个填充好的字符串缓冲区,在每次循环中重用。
  • 标准输出流被锁保护着,所以,我们不采用不断地获取、释放的形式,相反的,我们用 lock 进行数据写入同步。
  • 我们用平台原生的 std::ffi::OsStringstd::borrow::Cow 去避免不必要的空间分配

我唯一能做的事情就是 删除一个不必要的 mut

这是我这次经历的一个总结:

看似简单的 yes 程序其实没那么简单,它用了一个输出缓冲和内存对齐形式去提高性能。重新实现 Unix 工具很有意思,我很欣赏那些让电脑运行飞速的有趣的小技巧。

附上原文

A Little Story About the yes Unix Command

What's the simplest Unix command you know?
There's echo,which prints a string to stdout andtrue,which always terminates with an exit code of 0.

Among the rows of simple Unix commands,there's alsoyes. If you run it without arguments,you get an infinite stream of y's,separated by a newline:

y
y
y
y
(...you get the idea)

What seems to be pointless in the beginning turns out to be pretty helpful :

yes | sh boring_installation.sh

Ever installed a program,which required you to type "y" and hit enter to keep going?yesto the rescue! It will carefully fulfill this duty,so you can keep watchingPootie Tang.

Writing yes

Here's a basic version in... uhm... BASIC.

10 PRINT "y"
20 GOTO 10

And here's the same thing in Python:

while True:
    print("y")

Simple,eh? Not so quick!
Turns out,that program is quite slow.

python yes.py | pv -r > /dev/null
[4.17MiB/s]

Compare that with the built-in version on my Mac:

yes | pv -r > /dev/null
[34.2MiB/s]
So I tried to write a quicker version in Rust. Here's my first attempt:

use std::env;

fn main() {
  let expletive = env::args().nth(1).unwrap_or("y".into());
  loop {
    println!("{}",expletive);
  }
}

Some explanations:

  • The string we want to print in a loop is the first command line parameter and is named expletive. I learned this word from the yes manpage.
    1. use unwrap_or to get the expletive from the parameters. In case the parameter is not set,we use "y" as a default.
  • The default parameter gets converted from a string slice (&str) into an owned string on the heap (String) using into().

Let's test it.

cargo run --release | pv -r > /dev/null
   Compiling yes v0.1.0
    Finished release [optimized] target(s) in 1.0 secs
     Running `target/release/yes`
[2.35MiB/s]

Whoops,that doesn't look any better. It's even slower than the Python version! That caught my attention,so I looked around for the source code of a C implementation.

Here's the very first version of the program,released with Version 7 Unix and famously authored by Ken Thompson on Jan 10,1979:

main(argc,argc>1? argv[1]: "y");
}

No magic here.

Compare that to the 128-line-version from the GNU coreutils,which is mirrored on Github. After 25 years,it is still under active development! The last code change happened around a year ago. That's quite fast:

# brew install coreutils
gyes | pv -r > /dev/null 
[854MiB/s]

The important part is at the end:

/* Repeatedly output the buffer until there is a write error; then fail.  */
while (full_write (STDOUT_FILENO,bufused) == bufused)
  continue;

Aha! So they simply use a buffer to make write operations faster. The buffer size is defined by a constant namedBUFSIZ,which gets chosen on each system so as to make I/O efficient (see here). On my system,that was defined as 1024 bytes. I actually had better performance with 8192 bytes.

I've extended my Rust program:

use std::env;
use std::io::{self,BufWriter,Write};

const BUFSIZE: usize = 8192;

fn main() {
    let expletive = env::args().nth(1).unwrap_or("y".into());
    let mut writer = BufWriter::with_capacity(BUFSIZE,io::stdout());
    loop {
        writeln!(writer,expletive).unwrap();
    }
}

The important part is,that the buffer size is a multiple of four,to ensure memory alignment.

Running that gave me 51.3MiB/s. Faster than the version,which comes with my system,but still way slower than the results from this Reddit post that I found,where the author talks about 10.2GiB/s.

Update

Once again,the Rust community did not disappoint.
As soon as this post hit the Rust subreddit,user nwydo pointed out a previous discussion on the same topic. Here's their optimized code,that breaks the 3GB/s mark on my machine:

use std::env;
use std::io::{self,));
  process::exit(1);
}

Now that's a whole different ballgame!

  • We prepare a filled string buffer,which will be reused for each loop.
  • Stdout is protected by a lock. So,instead of constantly acquiring and releasing it,we keep it all the time.
  • We use a the platform-native std::ffi::OsString and std::borrow::Cow to avoid unnecessary allocations.

The only thing,that I could contribute was removing an unnecessary mut.

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


用的openwrt路由器,家里宽带申请了动态公网ip,为了方便把2280端口映射到公网,发现经常被暴力破解,自己写了个临时封禁ip功能的脚本,实现5分钟内同一个ip登录密码错误10次就封禁这个ip5分钟,并且进行邮件通知使用步骤openwrt为19.07.03版本,其他版本没有测试过安装bashmsmtpopkg
#!/bin/bashcommand1&command2&wait从Shell脚本并行运行多个程序–杨河老李(kviccn.github.io)
1.先查出MAMP下面集成的PHP版本cd/Applications/MAMP/bin/phpls-ls 2.编辑修改.bash_profile文件(没有.bash_profile文件的情况下回自动创建)sudovim~/.bash_profile在文件的最后输入以下信息,然后保存退出exportPATH="/Applications/MAMP/bin/php/php7.2.20/b
1、先输入locale-a,查看一下现在已安装的语言2、若不存在如zh_CN之类的语言包,进行中文语言包装:apt-getinstalllanguage-pack-zh-hans3、安装好后我们可以进行临时修改:然后添加中文支持: locale-genzh_CN.UTF-8临时修改> export LC_ALL='zh_CN.utf8'> locale永久
BashPerlTclsyntaxdiff1.进制数表示Languagebinaryoctalhexadecimalbash2#[0~1]0[0~7]0x[0~f]or0X[0~f]perl0b[0~1]0[0~7]0x[0~f]tcl0b[0~1]0o[0~7]0x[0~f]bashdifferentbaserepresntationreference2.StringlengthLanguageStr
正常安装了k8s后,使用kubect工具后接的命令不能直接tab补全命令补全方法:yum-yinstallbash-completionsource/usr/share/bash-completion/bash_completionsource<(kubectlcompletionbash)echo"source<(kubectlcompletionbash)">>~/.bashrc 
参考这里启动jar包shell脚本修改过来的#!/bin/bash#默认应用名称defaultAppName='./gadmin'appName=''if[[$1&&$1!=0]]thenappName=$1elseappName=$defaultAppNamefiecho">>>>>>本次重启的应用:$appName<
#一个数字的行#!/bin/bashwhilereadlinedon=`echo$line|sed's/[^0-9]//g'|wc-L`if[$n-eq1]thenecho$linefidone<1.txt#日志切割归档#!/bin/bashcd/data/logslog=1.logmv_log(){[-f$1]&&mv$1$2
#文件增加内容#!/bin/bashn=0cat1.txt|whilereadlinedon=[$n+1]if[$n-eq5]thenecho$lineecho-e"#Thisisatestfile.\n#Testinsertlineintothisfile."elseecho$linefidone#备份/etc目录#
# su - oraclesu: /usr/bin/ksh: No such file or directory根据报错信息:显示无法找到文件 /usr/bin/ksh果然没有该文件,但是发现存在文件/bin/ksh,于是创建了一个软连接,可以规避问题,可以成功切换到用户下,但无法执行系统自带命令。$. .bash_profile-ksh: .: .b
history显示历史指令记录内容,下达历史纪录中的指令主要的使用方法如果你想禁用history,可以将HISTSIZE设置为0:#exportHISTSIZE=0使用HISTIGNORE忽略历史中的特定命令下面的例子,将忽略pwd、ls、ls-ltr等命令:#exportHISTIGNORE=”pwd:ls:ls-ltr:”使用HIS
一.命令历史  1.history环境变量:    HISTSIZE:输出的命令历史条数,如history的记录数    HISTFILESIZE:~/.bash_history保存的命令历史记录数    HISTFILLE:历史记录的文件路径    HISTCONTROL:     ignorespace:忽略以空格开头的命令
之前在网上看到很多师傅们总结的linux反弹shell的一些方法,为了更熟练的去运用这些技术,于是自己花精力查了很多资料去理解这些命令的含义,将研究的成果记录在这里,所谓的反弹shell,指的是我们在自己的机器上开启监听,然后在被攻击者的机器上发送连接请求去连接我们的机器,将被攻击者的she
BashOne-LinersExplained,PartI:Workingwithfileshttps://catonmat.net/bash-one-liners-explained-part-oneBashOne-LinersExplained,PartII:Workingwithstringshttps://catonmat.net/bash-one-liners-explained-part-twoBashOne-LinersExplained,PartII
Shell中变量的作用域:在当前Shell会话中使用,全局变量。在函数内部使用,局部变量。可以在其他Shell会话中使用,环境变量。局部变量:默认情况下函数内的变量也是全局变量#!/bin/bashfunctionfunc(){a=99}funcecho$a输出>>99为了让全局变量变成局部变量
1、多命令顺序执行;  命令1;命令2  多个命令顺序执行,命令之间没有任何逻辑联系&&  命令1&&命令2  逻辑与,当命令1正确执行,才会执行命令2||  命令1||命令2  逻辑或,当命令1执行不正确,才会执行命令2例如:ls;date;cd/home/lsx;pwd;who ddif=输入文件of=输
原博文使用Linux或者unix系统的同学可能都对#!这个符号并不陌生,但是你真的了解它吗?首先,这个符号(#!)的名称,叫做"Shebang"或者"Sha-bang"。Linux执行文件时发现这个格式,会把!后的内容提取出来拼接在脚本文件或路径之前,当作实际执行的命令。 Shebang这个符号通常在Unix系统的脚本
1、历史命令history[选项][历史命令保存文件]选项:-c:  清空历史命令-w:  把缓存中的历史命令写入历史命令保存文件 ~/.bash_historyvim/etc/profile中的Histsize可改存储历史命令数量历史命令的调用使用上、下箭头调用以前的历史命令使用“!n”重复执行第n条历史
目录1.Shell脚本规范2.Shell脚本执行3.Shell脚本变量3.1环境变量3.1.1自定义环境变量3.1.2显示与取消环境变量3.1.3环境变量初始化与对应文件的生效顺序3.2普通变量3.2.1定义本地变量3.2.2shell调用变量3.2.3grep调用变量3.2.4awk调用变量3.3
   http://www.voidcn.com/blog/wszzdanm/article/p-6145895.html命令功能:显示登录用户的信息命令格式:常用选项:举例:w显示已经登录的用户及正在进行的操作[root@localhost~]#w 11:22:01up4days,21:22, 3users, loadaverage:0.00,0.00,0.00USER