文章目录
- 1.文件操作系统调用的几个基本接口
- open
- write
- read
- lseek
- write read close lseek ,对比C文件相关接口
- 2.如何理解文件操作?
- 3.文件描述符fd
- 文件描述符的分配规则
- 重定向
- 使用 dup2 系统调用进行重定向
- 4.在自己的shell中添加重定向功能:
1.文件操作系统调用的几个基本接口
这是C语言文件操作的博客博客链接。
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问:
open
函数原型:
#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
: 追加写
O_TRUNC
:清空文件原有内容
mode:设置新建文件的访问权限
umask 功能:查看或修改文件掩码 新建文件夹默认权限是0666、新建目录默认权限是0777 但实际上你所创建的文件和目录,看到的权限往往不是上面这个值。原因就是创建文件或目录的时候还要受到umask的影响。假设默认权限是mask,则实际创建的出来的文件权限是:
默认权限 & (~umask)
说明:将现有的存取权限减去权限掩码后,即可产生建立文件时预设权限。超级用户默认掩码值为0022,普通用户默认为0002。
我们设置mode的时候也要受umask的影响,如果设置位0666,则在普通用户下最后文件的权限就是0664。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
返回值:
RETURN VALUE
open() and creat() return the new file descriptor, or -1 if an error occurred
(in which case, errno is set appropriately).
成功:返回新打开的文件描述符(后面会有介绍是一个int类型的整数)
失败:-1
write
函数原型:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:
成功:返回写入的字节数,0表示什么都没写入
失败:返回-1,并设置全局变量errno
read
函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:
成功:返回读取的字节数,如果在调read之前已到达文件末尾,则这次read返回0
失败:返回-1并设置errno。
lseek
所有打开的文件都有一个当前文件偏移量(current file offset),以下简称为 cfo。cfo 通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。读写操作通常开始于 cfo,并且使 cfo 增大,增量为读写的字节数。文件被打开时,cfo 会被初始化为 0,除非使用了O_APPEND 。
使用 lseek 函数可以改变文件的 cfo 。
函数原型:
#include <unistd.h>
#include <sys/types.h>
off_t lseek(int filedes, off_t offset, int whence);
返回值:
成功:新的偏移量,
失败:返回-1并设置errno。
参数:
参数 offset 的含义取决于参数 whence:
- 如果 whence 是 SEEK_SET,文件偏移量将被设置为 offset。
- 如果 whence 是 SEEK_CUR,文件偏移量将被设置为 cfo 加上 offset,offset 可以为正也可以为负。
- 如果 whence 是 SEEK_END,文件偏移量将被设置为文件长度加上 offset,offset 可以为正也可以为负。
上面几个函数的应用代码:有一些比较重要的细节。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);//创建不存在的文件,然后清空文件里的内容int fd = open("log.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);//创建不存在的文件,然后追加文件// int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);// int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1; } //以\0为结尾,作为字符串结束标志是C语言规定的,//跟文件没有关系,所以往文件里写字符串的时候不需要考虑结尾加\0。 const char *msg= "I like Linux!"; write(fd , msg, strlen(msg)); lseek(fd,0,SEEK_SET );//重新定位char arr[64]; read(fd,arr,strlen(msg));arr[strlen(msg)+1]=0;printf("%s",arr);// fflush(stdout);close(fd);return 0;
}
write read close lseek ,对比C文件相关接口
先来认识一下两个概念: 系统调用 和 库函数。
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而open close read write lseek 都属于系统提供的接口,称之为系统调用接口。
来看下面的一张图:
系统调用接口和库函数的关系一目了然。可以认为,C语言文件操作的函数,都是对系统调用的封装,方便二次开发,系统调用更接近底层。同样,其他语言只要是在Linux系统上跑的话,就可以统一使用系统调用来进行文件操作。减少了我们的学习成本。
2.如何理解文件操作?
Linux下一切皆文件!
文件操作的本质:进程 和 被打开文件 的关系。
进程可以打开多个文件,系统中一定会存在大量的被打开的文件。被打开的文件需要被OS管理起来。
一说到管理那么就是先描述,再组织。
操作系统为了管理对应打开的文件,必定要为文件创建对应的内核数据结构(struct file { })来标识文件。它内部包含了文件的大部分属性。
任何一个进程删除该文件时,另外一个进程会立即出现读写失败这句话是错误的。
删除文件实际上只是删除文件的目录项,文件的数据以及inode并不会立即被删除,因此若进程已经打开文件,文件被删除时,并不会影响进程的操作,因为进程已经具备文件的描述信息。
3.文件描述符fd
- 通过对open函数的介绍,我们知道文件描述符就是一个小整数
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器
文件描述符是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了files_struct结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!
本质上,文件描述符就是该数组的下标。只要拿着文件描述符,就可以找到对应的文件。
所以输入输出还可以采用如下方式:
#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;
}
文件描述符的分配规则
直接看代码:
#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>
#include <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数组当中,从小到大按照顺序寻找最小的且没有被占用的fd,作为新的文件描述符。
重定向
推荐阅读
如果关闭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);
}
执行结果:fd:1
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。
常见的重定向有:>(输出重定向), >>(追加重定向), <(输入重定向)
类型 | 符号 | 用途 | 备注 |
---|---|---|---|
标准覆盖输出重定向 | 1> | 将命令执行的正确结果默认输出的位置,修改为指定的文件或者终端(覆盖原有内容) | 通常’>'即可,1可以不写,默认就是1 |
标准追加输出重定向 | >> | 将命令执行的正确结果,输出到指定文件的末尾(不覆盖原有内容) | - |
错误覆盖输出重定向 | 2> | 将命令执行的错误结果默认输出的位置,修改为指定的文件或者终端(覆盖原有内容) | - |
错误追加输出重定向 | 2>> | 将命令执行的错误结果,输出到指定文件的末尾(不覆盖原有内容) | - |
标准输入重定向 | 0< | 将命令中接收输入内容由默认的键盘,改为命令或者文件 | 通常’<'即可0可以写也可以不写,默认0 |
标准输入追加重定向 | 0<< | 将命令中接收输入内容由默认的键盘,改为命令或者文件 | - |
那重定向的本质是什么呢?
重定向的本质是:上层用的fd不变,在内核中更改fd对应的struct file*的地址,改变它指针的指向。
每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件.
使用 dup2 系统调用进行重定向
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2,dup是英文 duplicate拷贝 的缩写,拷贝的是当前调用dup2的进程所对应的,文件描述符下标里面的内容(file*所对应的值,也就是地址)。
Linux里对其功能的描述如下:
makes newfd be the copy of oldfd, closing newfd first if necessary
假如int fd=open("./log", O_CREAT | O_RDWR); dup2(fd,1)
,那么就是把fd里的内容拷贝到1里,让1也指向fd,那么最后本来要输入到1对应的文件里的内容,最后就输入到了fd对应的文件里。
示例代码:
#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;
}
4.在自己的shell中添加重定向功能:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>#define NUM 1024
#define OPT_NUM 64#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3#define trimSpace(start) do{\while(isspace(*start)) ++start;\}while(0)char lineCommand[NUM];
char *myargv[OPT_NUM]; //指针数组
int lastCode = 0;
int lastSig = 0;int redirType = NONE_REDIR;
char *redirFile = NULL;// "ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt" ->
void commandCheck(char *commands)
{assert(commands);char *start = commands;char *end = commands + strlen(commands);while(start < end){if(*start == '>'){*start = '\0';start++;if(*start == '>'){// "ls -a >> file.log"redirType = APPEND_REDIR;start++;}else{// "ls -a > file.log"redirType = OUTPUT_REDIR;}trimSpace(start);redirFile = start;break;}else if(*start == '<'){//"cat < file.txt"*start = '\0';start++;trimSpace(start);// 填写重定向信息redirType = INPUT_REDIR;redirFile = start;break;}else{start++;}}
}int main()
{while(1){redirType = NONE_REDIR;redirFile = NULL;errno = 0;// 输出提示符printf("用户名@主机名 当前路径# ");fflush(stdout);// 获取用户输入, 输入的时候,输入\nchar *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);assert(s != NULL);(void)s;// 清除最后一个\n , abcd\nlineCommand[strlen(lineCommand)-1] = 0; // ?//printf("test : %s\n", lineCommand);// "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n// "ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt" ->// "ls -a -l -i >> myfile.txt" -> "ls -a -l -i" "myfile.txt" ->// "cat < myfile.txt" -> "cat" "myfile.txt" ->commandCheck(lineCommand);// 字符串切割myargv[0] = strtok(lineCommand, " ");int i = 1;if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0){myargv[i++] = (char*)"--color=auto";}// 如果没有子串了,strtok->NULL, myargv[end] = NULLwhile(myargv[i++] = strtok(NULL, " "));// 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口// 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){if(myargv[1] != NULL) chdir(myargv[1]);continue;}if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0){if(strcmp(myargv[1], "$?") == 0){printf("%d, %d\n", lastCode, lastSig);}else{printf("%s\n", myargv[1]);}continue;}// 测试是否成功, 条件编译
#ifdef DEBUGfor(int i = 0 ; myargv[i]; i++){printf("myargv[%d]: %s\n", i, myargv[i]);}
#endif// 内建命令 --> echo// 执行命令pid_t id = fork();assert(id != -1);if(id == 0){// 因为命令是子进程执行的,真正重定向的工作一定要是子进程来完成// 如何重定向,是父进程要给子进程提供信息的// 这里重定向不会影响父进程,进程具有独立性switch(redirType){case NONE_REDIR:// 什么都不做break;case INPUT_REDIR:{int fd = open(redirFile, O_RDONLY);if(fd < 0){perror("open");exit(errno);}// 重定向的文件已经成功打开了dup2(fd, 0);}break;case OUTPUT_REDIR:case APPEND_REDIR:{umask(0);int flags = O_WRONLY | O_CREAT;if(redirType == APPEND_REDIR) flags |= O_APPEND;else flags |= O_TRUNC;int fd = open(redirFile, flags, 0666);if(fd < 0){perror("open");exit(errno);}dup2(fd, 1);}break;default:printf("bug?\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);}
}