对C的文件操作不了解的可以先看下面这篇博客:
C语言【文件操作】详解中(会使用fgetc,fputc,fgets,fputs,fscanf,fprintf,fread,fwrite函数)_fscanf fgets-CSDN博客
一、Linux下系统级的文件操作
打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到
1.一种传标志位的方法
cpp
#include <stdio.h>
#define ONE (1 << 0)
#define TWO (1 << 1)
#define THREE (1 << 2)
#define FOUR (1 << 3)
void Print(int flag)
{
if(flag & ONE)
printf("one\n");
if(flag & TWO)
printf("two\n");
if(flag & THREE)
printf("three\n");
if(flag & FOUR)
printf("four\n");
}
int main()
{
Print(ONE);
printf("\n");
Print(ONE|TWO);
printf("\n");
Print(ONE|TWO|THREE);
printf("\n");
Print(ONE|TWO|THREE|FOUR);
printf("\n");
Print(ONE|FOUR);
printf("\n");
return 0;
}
通过这种方法,就可以让一个函数根据不同的参数实现不同的功能(如:上面的代码中的if语句里面的代码快就可以设计成不同的功能)
2.open接口介绍

cpp
#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
*/
open的本质 是将文件数据(内容加属性)从硬盘拷贝到CUP的内存中(符合冯诺依曼体系)。
open函数具体使用哪个,和具体应用场景相关。
write read close lseek,类比c文件相关的接口
3.open函数的返回值
这里的返回值到底是什么呢??
在介绍这里的返回值时,先回顾一下两个概念:系统调用和库函数
- fopen fclose fread fwrite 都是C标准库当中的函数,称之为库函数
- open close read write lseek 都属于系统提供的接口,称之为系统调用接口

结论:f# 系列的函数,都是对系统调用的封装,方便二次开发
文件描述符fd
open函数的返回值就是文件描述符, 是一个整数
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2. 0,1,2对应的物理设备一般是:键盘,显示器,显示器
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
printf("stdin->fd:%d\n", stdin->_fileno); // 0
printf("stdout->fd:%d\n", stdout->_fileno); // 1
printf("stderr->fd:%d\n", stderr->_fileno); // 2
return 0;
}
文件描述符就是从0开始的小整数。 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件 。于是就有了file结构体 。表示一个已经打开的文件对象 。而进程执行 open 系统调用 ,所以必须让进程和文件关联起来。每个进程都有一个指针 *files ,指向一张表files_struct, 该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!!
所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

二、文件描述符的分配规则
先看代码案例:
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fda);
int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdb);
int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdc);
int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdd);
}
运行结果:
bash
[zhangsan@hcss-ecs-f571 IO]$ ./operfile1
fda:3
fdb:4
fdc:5
fdd:6
关闭0或者2,看一下fd是怎么分配的
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// 0 1 2
close(0);
int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fda);
int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdb);
int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdc);
int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdd);
}
运行结果:
bash
[zhangsan@hcss-ecs-f571 IO]$ ./operfile1
fda:0
fdb:3
fdc:4
fdd:5
关闭2:
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// 0 1 2
close(2);
int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fda);
int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdb);
int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdc);
int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdd);
}
运行结果:
cpp
[zhangsan@hcss-ecs-f571 IO]$ ./operfile1
fda:2
fdb:3
fdc:4
fdd:5
文件描述符的分配规则:
在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
三、重定向的本质
关闭文件描述符1,看一下会有什么效果
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// 0 1 2
close(1);
int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fda);
int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdb);
int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdc);
int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n", fdd);
}
运行会发现,打印结果没有打到屏幕上,而是打印到了log1.txt文件中

这个因为log1.txt文件描述符占了1位置,所以内容就被输入到了log1.txt文件中。
重定向的本质:
- open函数打开的模式不同
- 文件描述符的修改

四、dup2系统调用

cpp
#include <unistd.h>
int dup2(int oldfd, int newfd);
作用:将oldfd文件描述符里面的内容拷贝到newfd中
用dup2实现一下输入重定向
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// close(0); // 关闭stdin
int fd = open("log1.txt", O_RDWR);
printf("fd:%d\n", fd);
dup2(fd, 0);
char buffer[64];
fgets(buffer, sizeof(buffer), stdin);
printf("%s\n", buffer);
// 0 1 2
// close(1);
}
这段代码是将文件中的内容输入到屏幕上

五、在自主shell中添加重定向功能
原理:
- 检查输入的字符串中有无重定向的字符:">" 或者 ">>" 或者 "<"
- 若有,则在子进程中调用dup2函数来进行重定向
若没有,则正常执行代码
实现重定向功能代码:
cpp
33 // ls -a -l -> "ls -a -l" && "xxx.txt" && 重定向的方式
34 #define NoneRedir 0
35 #define InputRedir 1
36 #define AppRedir 2
37 #define OutputRedir 3
38
39 int redir_type = NoneRedir; // 记录正在执行的程序,重定向的方式
40 char* filename = NULL; // 保存重定向的目标文件
41
42 // 这里写成宏函数,就不用传二级指针了,而是直接替换到下面的代码中
43 // 下面这里是一个编程技巧,可以避免替换后因为有分号的音响
44 // 也可以把代码快写在{}中
45
46 #define Trimspace(start) do {\
47 while(isspace(*start))\
48 ++start;\
49 } while(0)
cpp
119 // ls -a -l >> xx.txt || ls -a -l >> xx.txt || cat < log.txt || ls -a -l
120 void parseRedir(char commandline[])
121 {
122 redir_type = NoneRedir;
123 filename = NULL;
124 char *start = commandline;
125 char *end = commandline + strlen(commandline);
126 while(start < end)
127 {
128 if(*start == '>')
129 {
130 if(*(start + 1) == '>')
131 {
132 // 追加重定向
133 *start = '\0';
134 start++;
135 *start = '\0';
136 start++;
137 Trimspace(start);// 去掉左边的空格
138 redir_type = AppRedir;
139 filename = start;
140 break;
141 }
142 // 输出重定向
143 *start = '\0';
144 start++;
145 Trimspace(start);
146 redir_type = OutputRedir;
147 filename = start;
148 break;
149 }
150 else if(*start == '<')
151 {
152 // 输入重定向
153 *start = '\0';
154 start++;
155 Trimspace(start);
156 redir_type = InputRedir;
157 filename = start;
158 break;
159 }
160 else
161 {
162 // 没有重定向
163 start++;
164 }
165 }
166 }
cpp
239 int ExecuteCommand()
240 {
241 // 不能让bash执行程序替换函数,这里需要创建子进程
242 pid_t id = fork();
243 if(id < 0)
244 return -1;
245 else if(id == 0)
246 {
247 // 子进程 如何执行?
248 int fd = -1;
249 if(redir_type == NoneRedir)
250 {
251 // Do Nothing
252 }
253 else if(redir_type == OutputRedir)
254 {
255 // 输出重定向
256 fd = open(filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);
257 dup2(fd, 1);
258 }
259 else if(redir_type == AppRedir)
260 {
261
262 fd = open(filename, O_WRONLY | O_CREAT, 0666);
263 dup2(fd, 1);
264 }
265 else if(redir_type == InputRedir)
266 {
267
268 fd = open(filename, O_RDONLY, 0666);
269 dup2(fd, 0);
270 }
271 else
272 {
273 // Bug ??
274 }
275 execvpe(gargv[0], gargv, genv);
276 exit(1); // 程序替换失败,退出码设为1
277 }
278 else
279 {
280 // 父进程等子进程
281 int status = 0;
282 pid_t rid = waitpid(id, &status, 0);
283 if(rid > 0)
284 {
285 // 等待成功
286 // printf("wait child process success!\n");
287 lastcode = WEXITSTATUS(status);
288 }
289 }
290 return 0;
291 }
自主shell实现的所有代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <iostream>
#include <ctype.h>
#include <string>
#define MAXSIZE 128
#define MAXARGS 32
// 传说中的命令行参数表
// shell自己内部维护的第一张表
// 命令行参数表
char* gargv[MAXARGS];
int gargc = 0;
// 环境变量表
char* genv[MAXSIZE];
int genvc = 0;
// 分隔符
const char* gsep = " ";
// shell自己所处的工作路径
char cwd[MAXSIZE];
//最近一个命令执行完毕,退出码
int lastcode = 0;
// ls -a -l -> "ls -a -l" && "xxx.txt" && 重定向的方式
#define NoneRedir 0
#define InputRedir 1
#define AppRedir 2
#define OutputRedir 3
int redir_type = NoneRedir; // 记录正在执行的程序,重定向的方式
char* filename = NULL; // 保存重定向的目标文件
// 这里写成宏函数,就不用传二级指针了,而是直接替换到下面的代码中
// 下面这里是一个编程技巧,可以避免替换后因为有分号的音响
// 也可以把代码快写在{}中
#define Trimspace(start) do {\
while(isspace(*start))\
++start;\
} while(0)
void LoadEnv()
{
// 正常情况下,环境变量表内部是从环境变量里来的
// 这里从父进程中取
extern char **environ;
for(;environ[genvc]; genvc++)
{
genv[genvc] = (char*)malloc(sizeof(char)*4096);
// genv[genvc] = new char[4096];
strcpy(genv[genvc], environ[genvc]);// 这里必须拷贝过来,不能指向environ里面的地址处
}
genv[genvc] = NULL;
}
static std::string rfindDir(const std::string& p)
{
if(p == "/")
return p;
const std::string psep = "/";
auto pos = p.rfind(psep);
if(pos == std::string::npos)
return std::string();
return p.substr(pos + 1);
}
const char* GetUserName()
{
char* name = getenv("USER");
if(name == NULL)
return "None";
return name;
}
const char* GetHostName()
{
char* hostname = getenv("HOSTNAME");
if(hostname == NULL)
return "None";
return hostname;
}
const char* GetPwd()
{
//char* pwd = getenv("PWD");
char* pwd = getcwd(cwd, sizeof cwd); // 这里是绝对路径 /home/zhangsan
if(pwd == NULL)
return "None";
return pwd;
}
void PrintCommanLine()
{
printf("[%s@%s %s]# ",GetUserName(), GetHostName(),rfindDir(GetPwd()).c_str()); // 用户名 @ 主机名 当前路径
fflush(stdout);
}
int GetCommand(char commandline[], int size)
{
if(NULL == fgets(commandline, size, stdin))
return 0;
//2.1用户在输入的时候,至少会按一次回车abcd\n,\n是一个字符
commandline[strlen(commandline)-1] = '\0';
return strlen(commandline);
}
// ls -a -l >> xx.txt || ls -a -l >> xx.txt || cat < log.txt || ls -a -l
void parseRedir(char commandline[])
{
redir_type = NoneRedir;
filename = NULL;
char *start = commandline;
char *end = commandline + strlen(commandline);
while(start < end)
{
if(*start == '>')
{
if(*(start + 1) == '>')
{
// 追加重定向
*start = '\0';
start++;
*start = '\0';
start++;
Trimspace(start);// 去掉左边的空格
redir_type = AppRedir;
filename = start;
break;
}
// 输出重定向
*start = '\0';
start++;
Trimspace(start);
redir_type = OutputRedir;
filename = start;
break;
}
else if(*start == '<')
{
// 输入重定向
*start = '\0';
start++;
Trimspace(start);
redir_type = InputRedir;
filename = start;
break;
}
else
{
// 没有重定向
start++;
}
}
}
int ParseCommand(char commandline[])
{
// 每次进来清空gargv表
gargc = 0;
memset(gargv, 0, sizeof gargv);
// 故意写成=
// 单个命令也没有错误,while语句里面会加1的
gargv[0] = strtok(commandline, gsep);
while((gargv[++gargc] = strtok(NULL, gsep)));
// printf("gargc:%d\n", gargc);
// int i = 0;
// for(;gargv[i]; i++)
// {
// printf("gargv[%d]:%s\n",i,gargv[i]);
// }
return gargc;
}
// return val:
// 0 :不是内建命令
// 1 :是内建命令 && 执行完毕(没有风险的)
int CheckBuiltinExecute()
{
if(strcmp(gargv[0],"cd") == 0 )
{
// 内建命令
if(gargc == 2)
{
// 新的目标路径:gargv[1]
// 1.更改进程内核中的路径
chdir(gargv[1]);
// 2.更改环境变量
char pwd[1024];
getcwd(pwd, sizeof(pwd));
// 这里避免"PWD="被覆盖(因为putenv不会拷贝字符串,而是直接使用传入指针指向的内存)
// 需要开辟新的空间,这里加1是因为字符串需要以"\0"结尾。
char* new_pwd = (char*)malloc(strlen("PWD=") + strlen(pwd) + 1);
snprintf(new_pwd, strlen("PWD=") + strlen(pwd) + 1, "PWD=%s", pwd);// PWD=/home/zhangsan
putenv(new_pwd);// 导出环境变量
// 修改自己的环境变量
genv[genvc++] = new_pwd;
genv[genvc] = NULL;
lastcode = 0;
}
return 1;
}
else if(strcmp(gargv[0], "echo") == 0) // cd, echo, env, export ... 内建命令
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
// $? ? -> 看做一个变量名
if(strcmp(gargv[1] + 1, "?") == 0)
{
printf("lastcode:%d\n", lastcode);
}
else if(strcmp(gargv[1] + 1, "PATH") == 0)
{
// 其他环境变量名可以自己添加
// 不用 getenv和putenv,可以直接在自己的genv中查找
printf("%s\n", getenv("PATH"));
}
lastcode = 0;
}
}
return 1;
}
return 0;
}
int ExecuteCommand()
{
// 不能让bash执行程序替换函数,这里需要创建子进程
pid_t id = fork();
if(id < 0)
return -1;
else if(id == 0)
{
// 子进程 如何执行?
int fd = -1;
if(redir_type == NoneRedir)
{
// Do Nothing
}
else if(redir_type == OutputRedir)
{
// 输出重定向
fd = open(filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);
dup2(fd, 1);
}
else if(redir_type == AppRedir)
{
fd = open(filename, O_WRONLY | O_CREAT, 0666);
dup2(fd, 1);
}
else if(redir_type == InputRedir)
{
fd = open(filename, O_RDONLY, 0666);
dup2(fd, 0);
}
else
{
// Bug ??
}
execvpe(gargv[0], gargv, genv);
exit(1); // 程序替换失败,退出码设为1
}
else
{
// 父进程等子进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
// 等待成功
// printf("wait child process success!\n");
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
int main()
{
// 0.从配置文件中获取环境变量表,填充环境变量表
// 这里从父进程里面取
LoadEnv();
char command_line[MAXSIZE] = {0};
while(1)
{
//1.打印命令行字符串
PrintCommanLine();
//2.获取用户输入
if(0 == GetCommand(command_line, sizeof(command_line)))
continue;
// printf("%s\n", command_line);
// 解析执行重定向的命令
// ls -a -l >> xx.txt || ls -a -l >> xx.txt || cat < log.txt || ls -a -l
// ls -a -l -> "ls -a -l" && "xxx.txt" && 重定向的方式
parseRedir(command_line);
printf("command:%s\n", command_line);
printf("redir_type:%d\n", redir_type);
printf("filename:%s\n", filename);
//3.解析字符串"ls -a -l" -> "ls" "-a" "-l"
//命令行解释器,需要对用户输入的字符串首先进行解析
ParseCommand(command_line);
//3.5 判断:这个命令是让父进程bash执行(内建命令),还是让子进程执行
if(CheckBuiltinExecute() > 0)
{
lastcode = 0;
// 是内建命令
continue;
}
//4.子进程执行这个命令
ExecuteCommand();
}
return 0;
}