在C语言中对于文件的操作有着几个常用的接口可以调用
fopen//打开文件
fclose//关闭文件
fprintf//输出
fscanf//输入
还有相对应不同功能的打开方式
关于各接口的详细介绍可以看看博主之前写的博客 C语言文件操作
事实上每一门高级语言都有其对应的文件操作接口,而它们所拥有的接口都是对操作系统本身对文件操作接口的封装,所以只要我们掌握了操作系统本身的文件操作接口那对于之后无论是学习哪一门高级语言的接口都可以很大程度的降低学习成本。
首先对文件进行操作肯定要先打开或关闭某一个文件,在Linux中对应的接口为
open//打开文件
close//关闭文件
open的第一个参数就是文件名,第二个参数是文件的打开方式,第三个是文件新建时的权限。
当文件打开成功时返回一个大于0的整数(文件描述符 下面会讲到),文件打开失败时返回-1
关闭文件只需要传入文件成功打开时的返回值
对于Linux系统的文件打开方式也是一样,可以看到flags是一个int类型,操作系统内部对各个打开方式进行了宏定义
O_RDONLY //只读打开
O_WRONLY //只写打开
O_RDWR //读写打开
//以上三个常亮,必须且只能指定一个
O_CREAT //若文件不存在则创建文件
O_APPEND //追加写入
如果想要多个选项一起,那就使用 | 运算符即可。
对于第三个参数 文件的权限,就是当指定打开的文件不存在时新建该文件后其对应的权限。如果新建出来的文件我们没有给定第三个参数那么文件的权限就会随机。一般而言普通文件的权限都是 0666 新建的,所以当我们不知道需要打开的文件是否存在时最好是在调用 open 时加上第三个参数。
下面看一段代码感受一下
#include
#include
#include
#include
#include
int main(){
//以只读且不存在创建的方式打开文件,新建文件的初始权限为0666
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
//判断是否打开成功
if(fd < 0){
perror("open");
return 1;
}
//关闭文件
close(fd);
return 0;
}
可以看到一开始并没有 log.txt这个文件,执行了程序后就创建出了一个新的且权限为 0666 的文件。
上面提到,当文件成功打开时返回文件的描述符,那么什么是文件描述符呢。
对文件的操作实际上是进程去完成的,一个进程可以打开多个文件。既然是进程去完成的操作,那么根据之前的学习我们知道操作系统对进程都是需要管理的,也就是操作系统会对进程进行 先组织再描述。那么操作系统为了可以管理起被打开的文件就一定会为其创建相对应的内核数据结构用来描述。在Linux中,通过
struct file{
}
描述每一个被打开的文件。*为了使进程能够执行open系统调用,所以必须让进程和文件关联起来,因此每个进程都会有一个 file指针去指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针。因此 文件描述符本质上就是该数组的下标。所以只需要拿到下标就可以找得到对应的文件
既然我们知道了文件描述符的意义,那么接下来就看看文件描述符是如何分配的,先来看看同时打开多个文件,它们所对应的文件描述符是什么
#include
#include
#include
#include
#include
int main(){
//以只读且不存在创建的方式打开文件,新建文件的初始权限为0666
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
printf("%d\n", fd1);
printf("%d\n", fd2);
printf("%d\n", fd3);
printf("%d\n", fd4);
//关闭文件
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
可以看到它们的文件描述符是有序的,那么既然是下标为什么不从0先开始呢。事实上,Linux进程默认情况下会有3个缺省打开的文件描述符,分别是:
0 //标准输入 -> 键盘
1 //标准输出 -> 显示器
2 //标准错误 -> 显示器
因此每一个被打开的文件的文件描述符都是从3依次记录的。相对应的如果我们将0、1、2关闭后再打开那么该文件的文件描述符就会从最小没有被占用的下标开始
认识了文件的打开关闭和文件描述符后,我们再来看看系统中对文件操作的其他接口。
write 是往文件里写入,第一个参数为文件描述符,第二个为指向需要写入数据,第三个为需要写入的字节数。
#include
#include
#include
#include
#include
int main(){
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
char* buf = "hello world!";
write(fd, buf, strlen(buf));
close(fd);
return 0;
}
read和write的调用是一样的。
既然可以关闭0、1、2的文件,那么现在我们关闭1试试看
#include
#include
#include
#include
#include
int main(){
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
printf("%d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
为什么执行之后没有显示出文件的描述符呢。这是因为文件描述符1虽然一开始是指向输出显示器的,但是当我们关闭之后再打开文件,这是1就不再是指向标准输出了而是指向了新打开的文件,所以也就不再输出到屏幕而是输出到了文件中。这就是输出重定向
那么重定向的本质是什么呢
没有发生重定向前呢,可以画图概括为
而当我们先关闭了1之后,1就不再指向标准输出了,再打开一个新的文件那么1就指向了新打开的文件
也就是说,重定向的本质就是:上层用的文件描述符不变,在内核中嘎变了文件描述符对应的 struct file* 的地址。
系统中也有对应的重定向接口,不需要我们每次都关闭某一个文件再打开新文件。
这个接口就可以直接实现文件的重定向,使用该接口后,重定向的文件描述符就会指向打开的文件。
#include
#include
#include
#include
#include
int main(){
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
dup2(fd, 1);
printf("%d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
根据这些新学的知识,就可以给之前写的shell进行功能增加了
> 输出重定向
>> 追加
< 输入重定向
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
#define OPT_NUM 100
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
//记录打开文件的规则
int redirType = NONE_REDIR;
//记录要打开的文件名
char* redirFile = NULL;
//记录输入字符串
char lineCommand[NUM];
//指针数组用于记录输入字符串的指令(不含选项)
char *myargv[OPT_NUM];
//记录退出状态和终止信号
int lastCode = 0;
int lastSig = 0;
//定义跳过空格函数
void trimSpace(char* start){
while(1){
if(*start == ' ')
start++;
else
break;
}
}
//定义重定向指令的分割方法
//将重定向的文件名获取
void commandCheck(char* com){
assert(com);
char* start = com;
char* end = com + strlen(com);
while(start < end){
if(*start == '>'){
*start = '\0';
start++;
//如果有两个> 说明是重定向追加输出
if(*start == '>'){
redirType = APPEND_REDIR;
start++;
}
else
redirType = OUTPUT_REDIR;
//需要将重定向符号后面的空格都跳过才可获取到文件名
trimSpace(start);
redirFile = start;
break;
}
else if(*start == '<'){
*start = '\0';
start++;
trimSpace(start);
redirType = INPUT_REDIR;
redirFile = start;
break;
}
else
start++;
}
}
int main(){
while(1){
//每次执行完一次重定向都需要更新一下值
redirFile = NULL;
redirType = NONE_REDIR;
errno = 0;
//输出提示符
printf("用户名@主机名 当前路径# ");
fflush(stdout);
//从stdin获取输入,输入结束要有'\n'
char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
assert(s != NULL);
(void)s;
//清除最后一个'\n'
lineCommand[strlen(lineCommand) - 1] = 0;
commandCheck(lineCommand);
//字符串切割,获取输入的指令
myargv[0] = strtok(lineCommand, " ");
int i = 1;
//将颜色选项放入ls命令中
if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
myargv[i++] = (char*)"--color=auto";
//没有子串的话,strtok返回NULL
//依次获取指令后面的选项
while(myargv[i++] = strtok(NULL, " "))
;
//cd命令不会创建子进程,就让shell自己执行对应命令,执行系统接口
if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){
if(myargv[1] != NULL)
//更改子进程当前工作目录
chdir(myargv[1]);
continue;
}
//echo $? 会打印出退出状态和终止信息
//其余的会将字符串打印出
if(myargv[0] != NULL && strcmp(myargv[0], "echo") == 0){
if(strcmp(myargv[1], "$?") == 0)
printf("%d, %d\n", lastCode, lastSig);
else
printf("%s\n", myargv[1]);
continue;
}
//执行命令
//创建子进程让子进程执行替换
pid_t id = fork();
assert(id != -1);
if(id == 0){
switch(redirType){
case NONE_REDIR:
break;
//如果是重定向输入那就直接打开文件重定向给0文件描述符
case INPUT_REDIR:
{
int fd = open(redirFile, O_RDONLY);
if(fd < 0){
perror("open");
exit(errno);
}
dup2(fd, 0);
}
break;
case OUTPUT_REDIR:
{
umask(0);
int fd = open(redirFile, O_WRONLY | O_CREAT, 0666);
if(fd < 0){
perror("open");
exit(errno);
}
dup2(fd, 1);
}
break;
case APPEND_REDIR:
{
umask(0);
int fd = open(redirFile, O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0){
perror("open");
exit(errno);
}
dup2(fd, 1);
}
break;
default:
printf("????\n");
break;
}
execvp(myargv[0], myargv);
exit(1);
}
//父进程等待接受子进程的信息
int status = 0;
pid_t ret = waitpid(id, &status, 0);
assert(ret > 0);
(void) ret;
lastCode = ((status>>8) & 0xFF);
lastSig = (status & 0x7F);
}
}