目录
一、预备知识
二、操作文件函数
三、操作文件系统调用
四、理解进程打开文件
函数 vs 系统调用
open的返回值 fd
如何理解一切皆文件?
理解struct file 内核对象
fd的分配规则 && 重定向
理解标准错误流(2号文件描述符)
一、预备知识
1. 文件 = 内容 + 属性, 对文件的操作无非就是对内容/属性操作
2. 内容是数据,属性也是数据,所以磁盘存储文件,既要存储文件内容,也要存储文件属性
3. C语言有打开访问关闭文件的库函数,而只有当程序运行起来时,cpu才会读取代码,执行对文件的操作,所以本质是进程访问了文件!
4. 一个进程可以打开多个文件,多个进程可以打开多个文件
5. 文件默认是在磁盘上的,加载磁盘上的文件,涉及到访问OS, 这个工作是OS进行的!
6. OS运行会打开多个文件,打开后就要对多个文件管理,先描述,再组织!
7. 文件无非分为打开了的文件和没有被打开的文件,所以对文件的学习就分为这两部分
文件打开前: 磁盘文件 文件打开后: 将文件加载到了内存中
8.研究内存中的文件的本质就是研究进程和被打开文件的关系!
二、操作文件函数
这属于C语言的知识点,我们快速回顾一下
打开文件,写入文件,关闭文件
新建写与追加写
文件以"w"方式打开,之前的内容会被清空,所以之前讲的 > log.txt 输出重定向就可以清空文件
文件以"a"方式打开,会从文件结尾开始写入, 不会清空文件内容
三、操作文件系统调用
进程打开文件涉及到了访问磁盘,所以必定是借助了OS的能力,而OS不相信任何用户,所以打开文件的函数底层一定封装了系统调用!
参数含义:
1. pathname: 打开的文件名
2. flags: 标志位 (可以传递各种宏的任意组合)
eg:
3. mode 表示打开文件指定的权限(开始的权限,文件最终权限还要去掉权限掩码)
返回值:
fd表示文件描述符,可以唯一标识文件
参数含义:
fd就是open的返回值,表示文件描述符,buf是要写入文件的字符串的起始地址,count表示要写入文件的字节数
返回值:
返回成功写入文件的字节个数
代码演示
上述所传的参数,是从头开始覆盖式地写入,文件内容并不会清空
要想打开文件后,原始内容清空,需要额外传递 O_TRUNC 参数
想要追加式写入,需要传递宏: O_APPEND
四、理解进程打开文件
函数 vs 系统调用
open的返回值 fd
当文件被打开后,就要从磁盘中加载到内存中,并且OS为了管理内存中的文件,要先描述,后组织,于是有了描述文件的结构体对象,同时进程启动后,有描述进程的task_struct结构体, 但是多个进程可能打开多个文件,如何知道哪个进程打开了哪个文件??
task_struct 中有一个指针变量,指向了一个结构体对象,结构体对象中有一个成员数组,这个数组中就存储了描述文件的结构体对象的地址,于是进程和文件本质还是解耦的,但是可以关联了!
而数组中存储的下标就叫做文件描述符!!
当调用write系统调用时,传递的id就是数组下标,这样进程就可以根据数组下标找到描述文件的结构体对象,找到文件并向文件写入了!
上面的数组下标是从3开始的,0, 1, 2下标呢??
一个程序运行起来之后,默认会打开三个流
标准输入 键盘 stdin 0
标准输出 显示器 stdout 1
标准错误 显示器 stderr 2
由上述操作文件的原理,我们知道了操作系统访问文件,只认文件描述符!!! 而C语言函数fopen返回值类型FILE*中的FILE是一个文件指针,FILE是一个C语言提供的结构体类型,FILE中必定封装了文件描述符!所以C语言的文件操作函数不仅实现方法进行了封装,返回值也进行了封装!!!
OS为啥要默认把三个流打开呢??? 之前写printf就可以直接打印消息到显示器上,scanf直接从键盘读取数据,Linux下一切皆文件, 而显示器文件和键盘文件我们并没有打开,所以默认打开的流,是为了让程序员进行输入输出代码编写!
如何理解一切皆文件?
所有文件核心工作都是输入输出,本质就是读写,而有的输入输出设备是只有读,或只有写,或读写都有,硬件的访问方式是不一样的,但是Linux下一切皆文件,所以一切设备的方法无非读写,这样就可以以统一的视角看待硬件,这其实就是多态!!!
理解struct file 内核对象
文件被打开后要加载到内存,文件=内容+属性,所以strcut file中一定要有指向属性和内容的字段, 用户读写文件的本质就是文件缓冲区和用户缓冲区的数据之间的拷贝!
fd的分配规则 && 重定向
现象:
结论: fd分配规则是寻找最小的,没有被使用的数据的位置,分配给指定的文件!
如上图所示,当关闭了标准输出流(1号文件描述符)之后,再次打开新的文件,根据fd分配规则,新的文件对应的文件描述符就是1了,所以此时数组1号下标的位置会被填充描述log.txt文件的结构体的地址,此时底层就发生了狸猫换太子的操作,但是用户并不知道,当你printf/fprintf 打印内容时,虽然printf底层封装的是stdout, 但本质OS只认文件描述符,而此时文件描述符1对应的内容已经变了,所以本来应该打印到显示器上的内容输出到了文件中,这就叫做输出重定向!
同样的,当0号文件描述符被关闭之后,再从stdin中读取数据,此时就变成了从log.txt中读数据了! 本来应该从键盘读取数据变成了从文件中读取数据,这叫叫做输入重定向!
上述重定向都要先close(文件描述符), 而OS提供了系统调用,使得我们不用关闭文件描述符,只需要拷贝覆盖就能完成重定向!所以重定向的本质就是文件描述符表中数组内容的拷贝!
dup2是系统调用,参数的含义是将oldfd指向的内容拷贝覆盖到newfd指向的内容!
支持重定向功能的自己实现的命令行解释器
上篇博客我们实现了一个命令行解释器bash, 但是不支持命令行级别的重定向功能!理解了重定向之后就可以把该功能加进来了!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NUM 1024 //命令行提示符个数限制
#define SIZE 64 //命令行提示符个数限制
#define SEP " " //strtok分隔符
//#define Debug 1 //对代码实现动态裁剪
//redir
#define NoneRedir 0
#define OutputRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir = NoneRedir;
char *filename = NULL;
char cwd[1024];
char enval[1024]; //for test
int lastcode = 0; //最近一个进程的退出码
const char* getUsername() //获取用户名
{
const char* name = getenv("USER");
if(name) return name;
else return "none";
}
const char* getHostname() //获取主机名
{
const char* hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char* getCwd() //获取当前路径
{
const char* cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
char* homepath() //获取家目录
{
char* home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
int getUserCommand(char* command, int num)
{
//输出命令行提示符
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd()); //获取环境变量
//char *fgets(char *s, int size, FILE *stream);
char* r = fgets(command, num, stdin); //从键盘获取用户指令, 用户最终一定会输入回车符(\n)
if(r == NULL) return -1;
//这里不会越界,因为当用户至少都要输入\n
command[strlen(command)-1] = '\0'; //去掉用户最后输入的\n
return strlen(command);
}
void commandSplit(char* in, char* out[])
{
int argc = 0;
//char *strtok(char *str, const char *delim);
out[argc++] = strtok(in, SEP);
while(out[argc++] = strtok(NULL, SEP));
#ifdef Debug
for(int i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
#endif
}
void cd(const char* path)
{
//int chdir(const char *path); 哪个进程调用chdir, 哪个进程的当前路径就会被修改!
chdir(path);
//char cwd[1024]; //不能写在此处,因为cd调用完后空间就释放了,环境变量就不是永久有效的了!
char tmp[1024];
//char *getcwd(char *buf, size_t size);
getcwd(tmp, sizeof(tmp)); //获取当前进程的绝对路径!
//int sprintf(char *str, const char *format, ...); 将本来应该打印到屏幕上的字符串格式化写入到str指向的空间中
sprintf(cwd, "PWD=%s", tmp); //将tmp以"PWD=%s"的格式写入到cwd中
putenv(cwd); //将cwd环境变量导入到当前进程的环境变量表中
}
//内建命令就是bash自己执行的类似于自己内部的一个函数!
//1->yes, 0->no,-1->err
int doBulidin(char* argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char* path = NULL;
if(argv[1] == NULL) path = homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval, argv[1]);//必须定义一个全局enval数组,否则导入环境变量之后,执行其他命令之后,usercommand就会重新覆盖写入,导入的环境变量就没了
putenv(enval);
return 1;
}
else if(strcmp(argv[0], "echo") == 0)
{
if(argv[1] == NULL)
{
printf("\n");
return 1;
}
if((*argv[1]) == '$' && strlen(argv[1]) > 1)
{
char* val= argv[1] + 1; //echo $PATH argv[1]是$, argv[1]+1是PATH的首地址
if(strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else
{
const char* enval = getenv(val);
if(enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else
{
printf("%s\n", argv[1]);
return 1;
}
}
//还有其他内建命令往后加分支语句即可!!
return 0;
}
int execute(char* argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0)
{
//程序替换不会影响重定向!!
int fd = 0;
if(redir == InputRedir)
{
fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir == OutputRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir == AppendRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
//DO Nonthing
}
//child
//exec command
//int execvp(const char *file, char *const argv[]);
execvp(argv[0], argv);
exit(1);
}
else
{
//father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) {
lastcode = WEXITSTATUS(status);
};
}
return 0;
}
#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; } while(0)
void checkRedir(char usercommand[], int len)
{
//ls -a -l < log.txt
//ls -a -l >> log.txt
char* end = usercommand + len - 1;
char* start = usercommand;
while(end > start)
{
if(*end == '>')
{
if(*(end-1) == '>') // >>
{
*(end-1) = '\0';
filename = end + 1;
SkipSpace(filename);
redir = AppendRedir;
break;
}
else
{
*end = '\0';
filename = end + 1;
SkipSpace(filename);
redir = OutputRedir;
break;
}
}
else if(*end == '<')
{
*end = '\0';
filename = end+1;
SkipSpace(filename); //如果有空格,就跳过
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
int main()
{
while(1)
{
redir = NoneRedir;
filename = NULL;
//1. 打印提示符&&获取命令行输入
char usercommand[NUM];
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue;
//printf("%s", usercommand);
//" ls -a -l > log.txt " -> 判断 -> "ls -a -l" redir_type "log.txt"
//1.1检查是否发生了重定向
checkRedir(usercommand, strlen(usercommand));
//2.对字符串做切割
char* argv[SIZE];
commandSplit(usercommand, argv);
//3.检查命令是否内建命令
n = doBulidin(argv);
if(n) continue; //内建命令由父进程直接执行,不用创建子进程
//4.创建子进程执行对应的命令
execute(argv);
}
return 0;
}
理解标准错误流(2号文件描述符)
现象:
为啥要标准错误流呢??
有的消息是常规消息,有的消息是错误消息,有了标准错误流,通过重定向,我们就可以将常规消息和错误消息分开,放在两个不同的文件中,方便排查错误信息!!!
C++中的cout和cerr也是同样的道理!