目录
一、重定向
1、输出重定向
在这段代码中,我们首先关闭了文件描述符1(通常是标准输出stdout),然后打开了一个新的文件log.txt
。由于文件描述符1被关闭,新打开的文件将会占用这个最小且未被使用的文件描述符,也就是1。
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
// 关闭文件描述符1,即关闭标准输出。
close(1);
// 打开"log.txt"文件。因为文件描述符1是最小的且当前未被使用的,它将被分配给"log.txt"。
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
// 如果打开文件失败,则打印错误信息并退出程序。
perror("open");
return 1;
}
// 此时,printf将输出到文件描述符1,也就是"log.txt"。
printf("fd: %d\n", fd); // 输出将被写入"log.txt"
close(fd);
return 0;
}
接着,通过printf
函数打印信息时,输出实际上被重定向到了log.txt
文件。这是因为printf
默认使用文件描述符1(stdout)进行输出,而现在文件描述符1指向了log.txt
而非标准输出。
运行这个程序后,你不会在控制台看到任何输出,因为printf
的输出已经被重定向到了log.txt
。如果你查看log.txt
(使用cat log.txt
),你会看到输出fd: 1
。
[hbr@VM-16-9-centos redirect]$ ./myfile
[hbr@VM-16-9-centos redirect]$ ls
buffer_rd.c log.txt makefile myfile myfile.c
[hbr@VM-16-9-centos redirect]$ cat log.txt
fd: 1
这个过程展示了重定向的本质:在操作系统内部更改文件描述符对应的目标。通过关闭和重新打开文件描述符,我们改变了标准输出的指向,从而实现了输出重定向。
2、输入重定向
输入重定向是一种将程序的输入从键盘转向文件或另一个程序的过程。在下面这个例子中,通过将"log.txt"文件作为程序的输入,实现了输入重定向。这样程序就不再从键盘读取输入,而是从"log.txt"文件中读取数据。
首先关闭了标准输入文件描述符(文件描述符0),然后使用open
函数以只读模式打开了"log.txt"文件,并将返回的文件描述符存储在fd
变量中。如果打开文件失败,会输出错误信息并返回1。
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
close(0); // 关闭标准输入文件描述符(文件描述符0)
// 打开"log.txt"文件,以只读模式打开
int fd = open("log.txt", O_RDONLY);
if (fd < 0) {
perror("open"); // 输出错误信息
return 1; // 返回错误码
}
printf("fd: %d\n", fd); // 打印文件描述符
char buffer[64];
// 从标准输入(实际上是从"log.txt"文件)中读取一行内容到buffer中
fgets(buffer, sizeof(buffer), stdin);
// 打印buffer中的内容
printf("%s\n", buffer);
return 0;
}
接着程序会打印出fd
的值,然后使用fgets
函数从标准输入(stdin)中读取最多sizeof(buffer)
个字符到buffer
数组中。最后,程序会打印出读取到的内容。
在执行程序后,可以看到程序输出了"fd: 0",表示成功打开"log.txt"文件并将其文件描述符存储在fd
中。然后程序从"log.txt"文件中读取了内容"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",并将其打印出来。
cat log.txt
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
./myfile
fd: 0
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
3、追加重定向
在打开文件后关闭了标准输出文件描述符(文件描述符1),然后用open
函数打开了一个名为"log.txt"的文件,设置了写入、追加和创建标志。接着使用fprintf
函数尝试往标准输出(stdout)写入内容,但实际上因为之前关闭了标准输出,所以内容被重定向到了"log.txt"文件中。
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(1); // 关闭标准输出文件描述符(文件描述符1)
// 打开或创建一个文件"log.txt",并以写入模式打开,
//如果文件不存在则创建它,如果文件已存在则在文件末尾追加写入
int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT);
if (fd < 0)
{
perror("open"); // 输出错误信息
return 1; // 返回错误码
}
// 将消息写入标准输出(实际上是写入"log.txt"文件,因为标准输出已被重定向)
fprintf(stdout, "you can see me\n");
return 0;
}
在执行程序后,可以看到"log.txt"文件中出现了"you can see me"这行内容。每次运行程序时,都会在"log.txt"文件中追加相同的内容。
[hbr@VM-16-9-centos redirect]$ ./myfile
[hbr@VM-16-9-centos redirect]$ ll
total 36
-rw-rw-r-- 1 hbr hbr 317 Mar 17 14:01 buffer_rd.c
-rw-rw-r-- 1 hbr hbr 360 Mar 17 14:49 input.c
-rw--wx--- 1 hbr hbr 15 Mar 17 14:54 log.txt
-rw-rw-r-- 1 hbr hbr 73 Mar 16 15:19 makefile
-rwxrwxr-x 1 hbr hbr 8560 Mar 17 14:54 myfile
-rw-rw-r-- 1 hbr hbr 310 Mar 17 14:54 myfile.c
-rw-rw-r-- 1 hbr hbr 674 Mar 17 14:18 output.c
[hbr@VM-16-9-centos redirect]$ cat log.txt
you can see me
[hbr@VM-16-9-centos redirect]$ ./myfile
[hbr@VM-16-9-centos redirect]$ ./myfile
[hbr@VM-16-9-centos redirect]$ ./myfile
[hbr@VM-16-9-centos redirect]$ ./myfile
[hbr@VM-16-9-centos redirect]$ cat log.txt
you can see me
you can see me
you can see me
you can see me
you can see me
4、dup2 系统调用
dup2
函数是一个系统调用,用于复制文件描述符。它的原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
参数说明
- oldfd:要复制的源文件描述符。
- newfd:目标文件描述符。如果该文件描述符已打开,
dup2
会先关闭它,然后再复制oldfd
。
返回值
- 如果成功,
dup2
返回newfd
。 - 如果发生错误,返回
-1
并设置errno
表明错误原因。
功能
-
文件描述符复制:
dup2
复制oldfd
到newfd
。- 如果
newfd
已经被打开,dup2
会先关闭newfd
,然后再复制oldfd
到newfd
。
-
文件描述符重定向:
- 常用于将文件描述符重定向到标准输入、输出或错误。
- 例如,可以将标准输出重定向到一个文件。
-
文件描述符管理:通过
dup2
可以有效地管理文件描述符,确保进程不会超过文件描述符的最大限制。
注意事项
-
文件描述符关闭:
- 如果
newfd
已经被打开,dup2
会先关闭它,然后再复制oldfd
。 - 如果您不希望关闭
newfd
,可以先手动关闭它。
- 如果
-
错误处理:总是检查
dup2
的返回值,并处理错误。 -
文件描述符限制:
- 每个进程可以打开的文件描述符数量有限制。
- 使用
dup2
时要确保不会超过这个限制。
示例:使用dup2函数配合命令行参数实现指定内容输出重定向到文件中
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
if(argc!=2)
{
return 2;
}
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC);
if(fd<0)
{
perror("open");
return 1;
}
dup2(fd,1);
fprintf(stdout,"%s\n",argv[1]);
return 0;
}
- 这段代码中,程序接受一个命令行参数,并将该参数写入到"log.txt"文件中。首先,程序检查命令行参数的数量是否为2,如果不是则返回2表示参数错误。
- 然后,程序尝试以写入方式打开"log.txt"文件,如果打开失败则打印错误信息并返回1。
- 接着,使用
dup2
函数将文件描述符fd
复制到文件描述符1(标准输出),这样所有标准输出都将被重定向到"log.txt"文件中。 - 最后,程序使用
fprintf
函数将命令行参数argv[1]
写入到标准输出(stdout),实际上是写入到"log.txt"文件中。 - 当你运行程序时,传递一个参数给程序,程序会将该参数写入"log.txt"文件中。每次运行程序并传递不同的参数,文件中的内容会被更新为最新的参数值。这样实现了将程序的输出重定向到文件中。
[hbr@VM-16-9-centos redirect]$ ./myfile hello
[hbr@VM-16-9-centos redirect]$ cat log.txt
hello
[hbr@VM-16-9-centos redirect]$ ./myfile world
[hbr@VM-16-9-centos redirect]$ cat log.txt
world
二、理性理解Linux系统下“一切皆文件”
在Linux中,一切皆文件的哲学深深植根于其设计之中。这一理念通过虚拟文件系统(VFS)得到体现,使得不同的硬件设备能够通过统一的接口与操作系统交互。在C语言环境下,虽然我们没有面向对象编程语言中的类和对象,但我们可以通过结构体(structs)和函数指针来模拟面向对象的特性,进而实现类似的封装和多态行为。
1. struct file
结构体
在 Linux 内核中,struct file
是用来表示一个已打开文件或设备的结构体。这个结构体定义在 <linux/fs.h>
中,并且包含了各种信息,如文件的状态、位置以及一组操作指针。关键的部分是 f_op
成员,它是一个指向 file_operations
结构体的指针,后者定义了针对该文件的一系列操作。
2. file_operations
结构体
file_operations
结构体定义了对文件进行操作的函数指针集合。例如,read
, write
, ioctl
, open
, release
等。每个驱动程序都会为它支持的设备类型定义一个 file_operations
实例,并将这个实例关联到对应的 struct file
上。
3. 抽象层
这种设计创建了一个抽象层,使得所有外部设备对于用户空间的应用程序来说看起来具有相同的接口。无论底层设备是磁盘、网络接口还是串行端口,用户空间程序都可以通过统一的文件操作来进行读写等操作。
4. 动态绑定
当一个文件被打开时,内核会根据文件类型查找合适的 file_operations
结构体,并将其绑定到 struct file
的 f_op
成员上。这意味着对于不同的设备,虽然它们可能有完全不同的底层实现,但它们共享相同的上层接口。
5. 链表管理
struct file
实例通常被组织成链表,这使得内核能够有效地跟踪系统中所有打开的文件。当需要执行某个操作时,内核可以通过遍历链表找到对应的 struct file
,进而调用相关的操作函数。
6. 设备驱动
每个设备驱动程序都会为它所支持的设备类型提供一套 file_operations
实现。例如,一个磁盘驱动可能会提供 read
和 write
函数,而一个网络设备驱动可能会提供 send
和 recv
函数。这些函数会被注册到 file_operations
结构体中,并在文件被打开时绑定到 struct file
上。
7. “一切皆文件”
这种设计方法体现了 Linux 中“一切皆文件”的哲学思想。无论是文件系统中的普通文件还是硬件设备,都被视为文件来处理。这种一致性极大地简化了程序设计,并使得内核可以以一种统一的方式来管理各种资源。
了解硬件接口
系统调用是操作系统提供给用户程序的接口,允许用户程序请求操作系统的服务,如文件操作、进程管理、通信等。这些调用形成了用户空间(用户程序运行的区域)和内核空间(操作系统核心部分运行的区域)之间的接口。
操作系统通过一系列的抽象层来管理硬件接口的操作。这些抽象层使用户程序不需要直接与硬件交互,提高了操作系统的可用性和安全性。下面是操作系统如何安排对硬件接口操作的基本概览:
-
硬件抽象层(HAL):硬件抽象层 (HAL) 是一个软件层,位于操作系统内核和其他系统软件之间,用于提供一个统一的接口来访问底层硬件。HAL 的主要目的是隐藏硬件的特异性和差异,从而使操作系统可以在多种不同的硬件平台上运行,而不需要为每种硬件编写特定的代码。
-
设备驱动程序:对于每种硬件设备(如硬盘、显卡、网络接口等),操作系统使用特定的设备驱动程序来进行通信。设备驱动程序负责将操作系统的通用操作转换为设备特定的指令,以及管理设备状态和执行操作系统的命令。
-
内核模式与用户模式:现代操作系统设计中,CPU提供了至少两种模式:内核模式(也称为监督模式或特权模式)和用户模式。操作系统内核和设备驱动程序在内核模式下运行,可以直接访问硬件资源。用户程序在用户模式下运行,不能直接访问硬件,必须通过系统调用来请求操作系统的服务。
-
中断和异常处理:操作系统使用中断(来自硬件设备的信号)和异常(来自CPU的错误或特殊情况信号)来响应外部事件或错误条件。当硬件设备需要CPU注意时(例如,数据已经从网络卡接收完毕),它会产生一个中断,操作系统会中断当前的处理流程,执行相应的中断处理程序,以响应和处理该事件。
-
系统调用和硬件操作:当用户程序执行系统调用请求操作系统服务时(如读写文件、发送网络数据包等),操作系统内核会根据请求的服务类型,通过调用相应的设备驱动程序和管理逻辑来操作硬件设备,完成用户程序的请求。
三、缓冲区
1、为什么要有缓冲区?
缓冲区是计算机内存中的一块区域,用于临时存储数据,以便在数据最终处理或传输之前对其进行批量处理。这块内存空间可以由操作系统、程序语言运行时环境或用户程序提供。
缓冲区的存在是为了提高系统的整体效率和加快对用户操作的响应速度。可以用小明发送快递给同学的例子来形象化:
- 当用户完成数据写入操作时,若无缓冲区,这就像小明每次都要亲自下楼、出门、乘坐火车或飞机将书送到同学手中,这种方式(相当于写透模式,Write Through:WT)不仅耗时长,成本也高。相反,拥有缓冲区就像小明将快递暂存到快递站,然后快递站负责集中派送,这样小明就可以迅速回到宿舍继续他的活动,大大节省了时间和精力(对应写回模式,Write Back:WB),既快速又降低了成本。
具体到计算机系统中,缓冲区的存在使得数据可以集中写入或读出,从而减少了对磁盘或网络的频繁访问,这就像小明发快递,快递服务批量发送学生们的包裹,提高了效率和速度。