1. 文件描述符的分配规则
我们知道Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入stdin--0, 标准输出stdout--1, 标准错误stderr--2。0,1,2对应的物理设备一般是:键盘,显示器,显示器.接下来我们来研究文件描述符的分配规则,代码如下。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd = open("file.txt",O_CREAT|O_WRONLY);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd);
close(fd);
return 0;
}
输出发现是 fd: 3 ,我们再关闭0或者2,在看看结果。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
close(0);
int fd = open("file.txt",O_CREAT|O_WRONLY);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd);
close(fd);
return 0;
}
发现是结果是: fd: 0 ,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
2. 重定向
那如果关闭1呢?看代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
close(1);
int fd = open("file.txt",O_CREAT|O_WRONLY);
if(fd<0)
{
perror("open");
return 1;
}
char * message="hello world\n";
write(1,message,strlen(message));
close(fd);
return 0;
}
在这里我们先将1号文件关闭后再打开 file.txt 文件,之后向1号文件内写入字符串,编译运行之后来查看结果。
默认情况下1号文件是显示器,我们可以看到屏幕并没有打印字符串,很正常因为我们把屏幕关闭了,但它却把内容写到了file.txt内,这个现象就是输出重定向。
当我们把1号文件关闭后再创建了一个file.txt文件,该文件就会占据1号文件的位置,那么当我们向1号文件写入时,自然就写入到了file.txt里。
3. 使用 dup2 系统调用
显然对于先关闭再打开这样的操作还是很麻烦,其实只需要struct file* fd_array[ ]数组里对应下标的值拷贝一份放到目标下标里,就可以完成一次重定向。
dup2
函数是Linux系统调用的一部分,用于复制文件描述符。它的原型定义在<unistd.h>
头文件。
dup2
函数的作用是将文件描述符oldfd
复制到文件描述符newfd
。如果newfd
已经打开,它会先被关闭。如果newfd
等于oldfd
,则dup2
返回newfd
而不关闭它。dup2
是一个原子操作,这意味着它要么完全成功,要么完全失败,不会出现部分成功的情况。
dup2
函数在文件描述符的管理中非常有用,例如在重定向输入输出时。例如,可以使用dup2
将标准输出(STDOUT_FILENO
)重定向到一个网络套接字,或者将标准错误(STDERR_FILENO
)重定向到一个文件。
如果dup2
调用成功,它返回新的文件描述符;如果失败,则返回-1,并且errno
会被设置以指示错误原因。
在实际编程中,dup2
可以用来创建文件描述符的副本,或者在需要时关闭和重新打开文件描述符,以改变它们的行为或关联的文件。
示例代码如下
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd = open("file.txt",O_CREAT|O_WRONLY);
if(fd<0)
{
perror("open");
return 1;
}
//重定向
dup2(fd,1);
char * message="hello world\n";
write(1,message,strlen(message));
return 0;
}
4. 缓冲区
我们有以下代码,其中 printf 和 fwrite 均为库函数,而 write 为系统调用接口。
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行出结果:
如果对进程实现输出重定向,使用 ./test1 > file1.txt 命令, 我们发现结果变成了:
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。肯定和 fork有关! 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。 printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。 而我们放在缓冲区中的数据,就不会被立即刷新,但是进程退出之后,会统一刷新,写入文件当中。 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。 write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统 调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由C标准库提供。