🤣 爆笑教程 👉 《看表情包学Linux》👈 猛戳订阅 🔥

💭 写在前面:在上一章中,我们已经把 fd 的基本原理搞清楚了。本章我们将开始探索 fd 的应用特征,探索文件描述符的分配原则。讲解重定向,上一章是如何使用 fflush 把内容变出来的,介绍 dup2 函数,追加重定向和输入重定向的知识。最后我们讲解缓冲区,研究缓冲区的刷新策略。
👻 目录
在上一章中,我们已经把 fd 的基本原理搞清楚了,知道了 fd 的值为什么是 0,1,2,3,4,5...
也知道了 fd 为什么默认从 3 开始,而不是从 0,1,2,因为其在内核中属于进程和文件的对应关系。
使用数组来完成映射,0,1,2,3,4,5 就是数组的下标。现在感觉不足为奇了,简直是天经地义!
我们还知道了 fopen / fclose / fread / fwrite.. 都必须得用所对应的 0,1,2,3,4,5...
用这些接口来找到对应的 struct file 结构,进而访问到底层对应的读写方法。
最终我们回答了 stdin, stdout, stderr 和 0,1,2 是一一对应关系。
现在再回过头来看这段代码,应该能有不少新的认识了:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
}
🚩 运行结果如下:

我们既然已经把原理搞清楚了,接下来我们应该探索应用特征了。
我们需要探索以下三个问题:
① 文件描述符的分配原则
② 重定向的本质
③ 理解缓冲区
现在我们不想把 0,1,2 打开了,我们直接在 open 前 close 一下玩玩?
💬 代码演示:默认把 0,1,2 打开,那我们直接 close(0) 关掉它们,扼杀在摇篮里
int main(void)
{
close(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
}
🚩 运行结果如下:

现在我们再把 2 关掉,close(2) 看看:
int main(void)
{
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
}
🚩 运行结果如下:

所以,默认情况下 0,1,2 被打开,你新打开的文件默认分的就是 3 (因为 0,1,2 被占了) 。
如果把 0 关掉,给你的就是 0,如果把 2 关掉,给你的就是 2……
那是不是把 1 关掉,给你的就是 1 呢?我们来看看:
int main(void)
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
}
🚩 运行结果如下:

出乎意料啊,fd 居然不是 1,而是什么都没有,这是怎么回事呢?
原因很简单,1 是 stdout,printf 打印是往 stdout 打印的,你把 1 关了当然就没有显示了。
分配规则:从头遍历数组 fd_array[] ,找到一个最小的且没有被使用的下标分配给新的文件。
根据 fd 的分配规则,新的 fd 值一定是 1,所以虽然 1 不再指向对应的显示器了,但事实上已经指向了 log.txt 的底层 struct file 对象了。
但是结果没打印出来, log.txt 里也什么都没有:

至于为什么没有,我们现在暂且不去讲解。但我们可以通过一种方法把它 "变出来" :
实际上并不是没有,而是没有刷新,用 fflush 刷新缓冲区后,log.txt 内就有内容了。
⚡ 代码演示:fflush 刷新缓冲区
int main(void)
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
}
🚩 运行结果如下:

我们自己的代码中调用的就是 printf,printf 本来是往显示器打印的,
现在不往显示器打了,而是写到了文件里,它的 "方向" 似乎被改变了。
这……不就是 重定向 吗?
经过这段小代码,我们看到了重定向和缓冲区的身影,这些问题也是我们下面要展开讨论的内容!
本来要往显示器打印的,最终变成了向指定文件打印 → 重定向 (redirection)
如果我们要进行重定向,上层只认识 0,1,2,3,4,5 这样的 fd,我们可以在 OS 内部,通过一定的方式调整数组的特定下标的内容 (指向),我们就可以完成重定向操作!
上面的一堆数据,都是内核数据结构,只有 OS 有权限,所以其必须提供对应接口,比如 dup。
除了 dup,还有有一个 dup2,后者更复杂一些,我们今天主要介绍 dum2 来进行重定向操作!
$ man dmp2

int dup2(int oldfd, int newfd);
dup2 可以让 newfd 拷贝 oldfd,如果需要可以将 newfd 先关闭。
newfd 是 oldfd 的一份拷贝,将后者 (newfd) 的内容写入前者 (oldfd),最后只保留 oldfd。

至于参数的传递,比如我们要输出重定向 (stdout) 到文件中:
我们要重定向时,本质是将里面的内容做改变,所以是要把 fd 的内容拷贝到 1 中的:
当我们最后进行输出重定向的时候,所有的内容都和 fd 的内容是一样的了。
所以参数在传递时,oldfd 是 fd,所以应该是 dum2(fd, 1);
dum2(fd, 1); ✅
dum2(1, fd); ❌
因为要将显示器的内容显示到文件里,所以 oldfd 就是 fd,newfd 就是 1 了。
📌 注意事项:dum2() 接口在设计时非常地反直觉,所以在理解上特比容易乱,搞清楚原理!
按我们一般的理解,文件 open 后 0,1,2 是现被打开的,0,1,2 才应该是 oldfd。而后打开的 3,4,5... 应该是属于 newfd。但事实恰恰相反,0,1,2 才是 newfd,3,4,5... 反而是 old_fd,所以个人认为在命名上不是很好,容易让人掉坑。
💬 代码演示:dup2()
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
dup2(fd, 1); // fd ← 1
fprintf(stdout, "打开文件成功,fd: %d\n", fd);
// 暂时不做讲解,后面再说
fflush(stdout);
close(fd);
return 0;
}
🚩 运行结果如下:

我们可以加个 ret 来接收 dum2 的结果:
int ret = dup2(fd, 1);
if (ret > 0) {
close(fd);
}
printf("ret: %d\n", ret);

追加重定向只需要将我们 open 的方式改为 O_APPEND 就行了。
int main(void)
{
// 追加重定向只要将我们打开文件的方式改为 O_APPEND 即可
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0) {
perror("open");
return 0;
}
dup2(fd, 1);
fprintf(stdout, "打开文件成功,fd: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
🚩 运行结果如下:

之前我们是如何读取键盘上的数据的?
int main(void)
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 0;
}
// 读数据
char line[64];
while (fgets(line, sizeof(line),stdin) != NULL) {
printf("%s\n", line);
}
fflush(stdout);
close(fd);
return 0;
}
🚩 运行结果如下:

现在我们使用输入重新的,说白了就是将 "以前从我们键盘上去读" 改为 "在文件中读"。
💬 代码演示:所以我们将 open 改为 O_RDONLY,dup(fd, 0) :
int main(void)
{
// 输入重定向
int fd = open("log.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 0;
}
// 将本来从键盘上读 (0),改为从文件里读(3)
dup2(fd, 0);
// 读数据
char line[64];
while (fgets(line, sizeof(line),stdin) != NULL) {
printf("%s\n", line);
}
fflush(stdout);
close(fd);
return 0;
}
🚩 运行结果如下:

❓ 我们先提出三个问题:什么是缓冲区?为什么要有缓冲区?缓冲区在哪里?
对于缓冲区的概念,我们在 "进度条实现" 的插叙章节中有做探讨,但只是一个简单的讲解。
我们对缓冲区有一个共识,也知道它的存在,但我们还没有去深入理解它。
我们先来探讨第一个问题:① 什么是缓冲区?缓冲区的本质就是一段内存。
② 为什么要有缓冲区?为了 解放使用缓冲区的进程时间。
缓冲区的存在可以集中处理数据刷新,减少 IO 的次数,从而达到提高整机的效率的目的。
③ 缓冲区在哪里?我们写一段代码来感受 "缓冲区" 的存在!
💬 代码演示:用 printf 和 write 各自打印一段话
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
printf("Hello printf\n"); // stdout -> 1
const char* msg = "Hello write\n";
write(1, msg, strlen(msg));
sleep(5); // 休眠五秒
return 0;
}
🚩 运行结果如下:

但是,如果我们去除 \n,我们就会发现 printf 的内容没有被立马打印,而 write 立马就出来了:
int main(void)
{
printf("Hello printf"); // stdout -> 1
const char* msg = "Hello write";
write(1, msg, strlen(msg));
sleep(5);
return 0;
}

首先,我们要知道:printf 内部就是封装了 write!
printf 里打印的内容 "Hello printf" 实际上是在缓冲区里的,printf 不显示的原因是没有带 \n,数据没有被立即刷新,所以 sleep 时 printf 的内容没有被显示出来。
所以 printf 不带 \n,数据没有被立即刷新,原因是因为它有缓冲区,此时如果我们想让他刷新,可以手动加上 fflush(stdout) 刷新一下缓冲区。
至此我们说明了,printf 没有立即刷新的原因,是因为有缓冲区的存在。
可是,write 是立即刷新的!既然 printf 又封装了 write,那么缓冲区究竟在哪?
"我拷,好寄叭怪!"
通过上述现象我们可以可以明确的是:缓冲区不一定在 write 内!
这个缓冲区一定不在 write 内部,因为如果这个缓冲区是函数内部提供的,那么直接刷新出来了。
所以这个缓冲区它只能是 C 语言提供的,该缓冲区是一个 语言级缓冲区 (语言级别的缓冲区) 。
这就意味着我们曾经谈论的缓冲区,不是内核级别的缓冲区,而是一个语言级别的缓冲区。
我们再演示一次,这次选用 C 库函数 printf, fprintf 和 fputs,系统调用接口 write,观察其现象。
💬 代码演示:老样子,首先给它们都带上 \n
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
// 给它们都带上 \n
printf("Hello printf\n"); // stdout -> 1
fprintf(stdout, "Hello fprintf!\n");
fputs("Hello fputs!\n", stdout);
const char* msg = "Hello write\n";
write(1, msg, strlen(msg));
sleep(5);
return 0;
}
🚩 运行结果如下:

💬 代码演示:现在我们再把 \0 去掉:
int main(void)
{
printf("Hello printf"); // stdout -> 1
fprintf(stdout, "Hello fprintf!");
fputs("Hello fputs!", stdout);
const char* msg = "Hello write";
write(1, msg, strlen(msg));
sleep(5);
return 0;
}

此时结果是只有 write 内容先出,当退出时 printf, fprint, fputs 的东西才显示出来。
然而 write 无论带不带 \n 都会立马刷新,也就是说,只要 printf, fprint, fputs 调了 write 数据就一定显示。
我们继续往下深挖,stdout 的返回值是 FILE,FILE 内部有 struct,封装很多的成员属性,其中就包括 fd,还有该 FILE 对应的语言级缓冲区。
C 库函数 printf, fwrite, fputs... 都会自带缓冲区,但是 write 系统调用没有带缓冲区。
我们现在提及的缓冲区都是用户级别的缓冲区,为提高性能,OS 会提供相关的 内核级缓冲区。
(内核级缓冲区不在本章的探讨范围内,本质我们专注于用户级缓冲区)
库函数在系统调用的上层,是对系统调用做的封装,但是 write 没有缓冲区,这说明了:
该缓冲区是二次加上的,由 C 语言标准库提供,我们来看下 FILE 结构体:

放到缓冲区,当数据积累到一定程度时再刷。
如果在刷新之前关闭了 fd,会有什么问题?
int main(void)
{
printf("Hello printf"); // stdout -> 1
fprintf(stdout, "Hello fprintf!");
fputs("Hello fprintf!", stdout);
const char* msg = "Hello write";
write(1, msg, strlen(msg));
close(1); // 直接把内部的文件关掉了,看你怎么刷
sleep(5);
return 0;
}
🚩 运行结果如下:

之前的代码示例中,为了解决这个问题,我们用 fflush 冲刷缓冲区让数据 "变" 了出来:
int main(void)
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
}
重定向到文件中时不用 fflush,直接调 close 文件显示不出来的原因是:数据暂存到了缓冲区。
既然缓冲区在 FILE内部,在 C 语言中,我们每一次打开一个文件,都要有一个 FILE* 会返回。
这就意味着,每一个文件都有一个 fd 和属于它自己的语言级别缓冲区。
刷新策略,即什么时候刷新,刷新策略分为常规策略 和 特殊情况。
常规策略:
特殊情况:
下面我们来一个比较怪的问题,注意最后调用了一个 fork:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
const char* str1 = "hello printf\n";
const char* str2 = "hello fprintf\n";
const char* str3 = "hello fputs\n";
const char* str4 = "hello write\n";
// C 库函数
printf(str1);
fprintf(stdout, str2);
fputs(str3, stdout);
// 系统接口
write(1, str4, strlen(str4));
// 调用完了上面的代码,才执行的 fork
fork();
return 0;
}
🚩 运行结果如下:

到目前为止还很正常,四个接口分别输出对应的字符串,打印出 4 行,没问题。
但如果我们此时重定向,比如输入 ./myfile > log.txt,怪事就发生了!log.txt 中居然有 7 条消息:

💡 解读:当我们重定向后,本来要显示到显示器的内容经过重定向显示到了文件里,
然而这里重定向,由显示器重定向到了文件,缓冲区的刷新策略由 "行缓冲" 转变为 "全缓冲"。
" 当然,这和那个 fork 脱不了干系 "
文件中有 7 条,printf 出现 2 次,fprintf 出现 2 次,fputs 出现 2 次,但是 write 只有一次,
这和缓冲区有关,因为 write 压根不受缓冲区的影响。
fork 要创建子进程,之后父子进程无论谁先退出,它们都要面临的问题是:父子进程刷新缓冲区。
📌 刷新的本质:把缓冲区的数据 write 到 OS 内部,清空缓冲区。
这里的 "缓冲区" 是自己的 FILE 内部维护的,属于父进程内部的数据区域。
所以当我们刷新时,代码和数据要发生写实拷贝,即父进程刷一份,子进程刷一份,
因而导致上面的现象,printf, fprintf, fputs 刷了 2 次到了 log.txt 中。
(最后,至于缓冲区的应用,我们会增加一个 "插叙" 章节,模拟实现一个自己封装的 C 标准库)

📌 [ 笔者 ] 王亦优
📃 [ 更新 ] 2022.3.
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
本人也很想知道这些错误,恳望读者批评指正!
| 📜 参考资料 C++reference[EB/OL]. []. http://www.cplusplus.com/reference/. Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/. 比特科技. Linux[EB/OL]. 2021[2021.8.31 xi |
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信
我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A
我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只