基础IO_系统文件IO | 重定向【Linux】

发布于:2025-08-18 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、 理解"文件"

1、狭义理解

  • 文件在磁盘里
  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设(即是输出设备也是输入设备)
  • 磁盘上的文件 本质是对文件的所有操作,都是对外设的输入和输出 简称 IO

2、广义理解

  • Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)

3、文件操作的归类认知

  • 对于 0KB 的空文件是占用磁盘空间的
  • 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
  • 所有的文件操作本质是文件内容操作和文件属性操作

4、系统角度

  • 对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统
  • 文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的

二、回顾C文件接口

1、hello.c打开文件

#include <stdio.h>
int main()
{
	FILE *fp = fopen("myfile", "w");
	if(!fp)
	{
		printf("fopen error!\n");
	} 
	while(1);
	fclose(fp);
	return 0;
}

打开的myfile文件在哪个路径下?

xz@xzlinux:~$ ps ajx | head -1;ps ajx | grep catme
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
 336450  336595  336595  336435 pts/0     336595 R+    1000   0:02 ./catme
 336584  336599  336598  336569 pts/1     336598 S+    1000   0:00 grep --color=auto catme
xz@xzlinux:~$ ls /proc/336595 -l
total 0
......
-r--r--r--  1 xz xz 0 May  8 15:34 cpuset
lrwxrwxrwx  1 xz xz 0 May  8 15:34 cwd -> /home/xz/z/IOleran
-r--------  1 xz xz 0 May  8 15:34 environ
lrwxrwxrwx  1 xz xz 0 May  8 15:34 exe -> /home/xz/z/IOleran/catme
dr-x------  2 xz xz 4 May  8 15:34 fd
......

其中:

  • cwd:指向当前进程运行目录的一个符号链接。
  • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。

2、hello.c写文件

#include <stdio.h>
#include <string.h>
int main()
{
	FILE *fp = fopen("myfile", "w");
	if(!fp)
	{
		printf("fopen error!\n");
	} 
	const char *msg = "hello bit!\n";
	int count = 5;
	while(count--)
	{
		fwrite(msg, strlen(msg), 1, fp);
	} 
	
	fclose(fp);
	return 0;
}

3、hello.c读文件

#include <stdio.h>
#include <string.h>

int main()
{
	FILE *fp = fopen("myfile","r");
	if(!fp)
	{
		printf("fopen error!\n");
		return 1;
	}

	char buff[1024];
	const char *msg = "hello bit!\n";
	while(1)
	{
		ssize_t s = fread(buff,1,strlen(msg),fp);
		if(s > 0)
		{
			buff[s] = 0;
			printf("%s",buff);
		}
		if(feof(fp))
		{
			break;
		}
	}
	fclose(fp);
	return 0;
}

稍作修改,实现简单cat命令:

#include <stdio.h>
#include <string.h>

//简单实现cat命令
int main(int argc, char*argv[])
{
	if (argc != 2)
	{
		printf("argv error!\n");
		return 1;
	}
	FILE *fp = fopen(argv[1],"r");
	if(!fp)
	{
		printf("fopen error!\n");
		return 2;
	}
	char buf[1024];
	while(1)
	{
		int s = fread(buf,1,sizeof(buf),fp);
		if(s > 0)
		{
			buf[s] = 0;
			printf("%s",buf);
		}
		if(feof(fp))
		{
			break;
		}
	}
	fclose(fp);

	return 0;
}

4、输出信息到显示器,你有哪些方法

#include <stdio.h>
#include <string.h>

int main()
{
	const char *msg = "hello fwrite\n";
	fwrite(msg, strlen(msg), 1, stdout);

	printf("hello printf\n");
	fprintf(stdout,"hello fprintf\n");

	return 0;
}

5、stdin & stdout & stderr

#include <stdio.h>

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

6、打开文件的方式

r      Open text file for reading.  The stream is positioned at  the  beginning  of  the
              file.

       r+     Open  for  reading and writing.  The stream is positioned at the beginning of the
              file.

       w      Truncate file to zero length or create text file for writing.  The stream is  po‐
              sitioned at the beginning of the file.

       w+     Open  for  reading and writing.  The file is created if it does not exist, other‐
              wise it is truncated.  The stream is positioned at the beginning of the file.

       a      Open for appending (writing at end of file).  The file is created if it does  not
              exist.  The stream is positioned at the end of the file.

       a+     Open  for reading and appending (writing at end of file).  The file is created if
              it does not exist.  Output is always appended to the end of the file.   POSIX  is
              silent on what the initial read position is when using this mode.  For glibc, the
              initial  file  position  for reading is at the beginning of the file, but for An‐
              droid/BSD/MacOS, the initial file position for reading is at the end of the file.

如上,是文件相关操作。还有 fseek ftell rewind 的函数,在C部分已经有所涉猎。

三、系统文件I/O

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:

1、一种传递标志位的方法

#include <stdio.h>
#include <string.h>

#define ONE   0001  //0000 0001
#define TWO   0002  //0000 0001
#define THREE 0004  //0000 0001

void func(int flags)
{
	if (flags & ONE) 
		printf("flags has ONE!");
	if (flags & TWO) 
		printf("flags has TWO!");
	if (flags & THREE) 
		printf("flags has THREE!");

	printf("\n");
}

int main()
{
	func(ONE);
	func(THREE);
	func(ONE | THREE);
	func(ONE | THREE | TWO);

	return 0;
}

操作文件,除了上面的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面一模一样的代码:

2、hello.c 写文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
	umask(0);
	int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
	if (fd < 0) 
	{
		perror("open");
		return 1;
	} 
	int count = 5;
	const char* msg = "hello xz!\n";
	int len = strlen(msg);

	while (count--) 
	{
		write(fd, msg, len);
		//fd: 后面讲, msg:缓冲区首地址。
		//len: 本次读取,期望写入多少个字节的数据。 
		//返回值:实际写了多少字节数据
	} 

	close(fd);
	return 0;
}

3、hello.c读文件

#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("myfile", O_RDONLY);
	if (fd < 0) 
	{
		perror("open");
		return 1;
	} 
	const char* msg = "hello bit!\n";
	char buf[1024];

	while (1) 
	{
		//ssize_t
		ssize_t s = read(fd, buf, strlen(msg));//类比write
		if (s > 0) 
		{
			printf("%s", buf);
		}
		else 
		{
			break;
		}
	} 

	close(fd);
	return 0;
}

4、接口介绍

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

pathname: 要打开或创建的⽬标⽂件
flags: 打开⽂件时,可以传⼊多个参数选项,flags。
参数:
			O_RDONLY: 只读打开
			O_WRONLY: 只写打开
			O_RDWR : 读,写打开
			这三个常量,必须指定⼀个且只能指定⼀个
			O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问
权限
			O_APPEND: 追加写
返回值:
			成功:新打开的⽂件描述符
			失败:-1
  • mode_t理解:直接 man 手册,比什么都清楚。
  • open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
  • write read close lseek ,类比C文件相关接口。

5、open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用库函数

  • 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数
    (libc)。
  • open close read write lseek 都属于系统提供的接口,称之为系统调用接口
  • 回忆一下我们讲操作系统概念时,画的一张图
    在这里插入图片描述

系统调用接口和库函数的关系,一目了然。
所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。

6、文件描述符fd

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数

6.1、0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器
    所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
	char buf[1024];
	ssize_t s = read(0, buf, sizeof(buf));
	if(s > 0)
	{
		buf[s] = 0;
		write(1, buf, strlen(buf));
		write(2, buf, strlen(buf));
	} 
	return 0;
}

在这里插入图片描述

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

对于以上原理结论我们可通过内核源码验证:

首先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h(3.10.0-1160.71.1.el7.x86_64是内核版本,可使用 uname -a 自行查看服务器配置, 因为这个文件夹只有一个,所以也不用刻意去分辨,内核版本其实也随意)

  • 要查看内容可直接用vscode在windows下打开内核源代码
  • 相关结构体所在位置

struct task_struct/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h
struct files_struct/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fdtable.h
struct file/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h
在这里插入图片描述

6.2、文件描述符的分配规则

直接看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	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>
#inc
lude <sys/stat.h>
#include <fcntl.h>
int main()
{
	close(0);
	//close(2);
	int fd = open("myfile", O_RDONLY);
	if(fd < 0)
	{
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	
	close(fd);
	return 0;
}

发现是结果是: fd: 0 或者 fd: 2 ,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

6.3、重定向

那如果关闭1呢?看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
	close(1);
	int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
	if(fd < 0)
	{
		perror("open");
		return 1;
	} 
	printf("fd: %d\n", fd);
	
	fflush(stdout);
	close(fd);
	exit(0);
}

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有: > , >> , <

那重定向的本质是什么呢?

6.4、使用 dup2 系统调用

函数原型如下:

#include <unistd.h>

int dup2(int oldfd, int newfd); 

示例代码

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() 
{
	int fd = open("./log", O_CREAT | O_RDWR);
	if (fd < 0) 
	{
		perror("open");
		return 1;
	}
	close(1);
	dup2(fd, 1);
	
	for (;;) 
	{
		char buf[1024] = {0};
		ssize_t read_size = read(0, buf, sizeof(buf) - 1);
		if (read_size < 0) 
		{
			perror("read");
			break;
		} 
		printf("%s", buf);
		fflush(stdout);
	} 
	return 0;
}

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

6.5、在minishell中添加重定向功能

重定向myshell—https://gitee.com/xiaozhi


网站公告

今日签到

点亮在社区的每一天
去签到