引言
C语言进阶 文件管理
上一篇文章详细回顾了C语言方面关于文件操作的一些库函数,比如输入输出重定向fscanf、fprintf,对于文件内容以字符形式读取的fgetc、fputc,对于文件内容以字符串形式读取的fgets、fputs,对于二进制文件的数据读取fread、fwrite,前几个函数说的都是关于文件顺序读写的一些操作函数,还有操作于文件指针层面的随机读写:ftell()返回文件指针相对于起始位置的偏移量、frewind()将文件指针置于起始位置、fseek()将文件指针相对于(SEEK_SET | SEEK_CUR | SEEK_END)三种位置offset个偏移量的搬运,以及对于文件权限的了解 w r a w+ r+ a+ wb rb ab wb+ rb+ ab+ 有了上一篇文章关于文件操作库函数的学习,这篇博文引入对于系统调用接口的相关学习。
我们都知道库函数就是相对于系统调用接口进行的进一步封装,而系统调用接口是操作系统为了向上层操作提供的一些用于访问内核指定功能的接口。
正文
目录
一、系统调用接口介绍
1、open(char* pathname, int flag, mode_t mode)
第二个参数:
第三个参数:修改文件权限
文件掩码:
2、ssize_t write(int fd,char* buf, size_t len)
3、ssize_t read(int fd,char* buf, size_t len)
4、off_t lseek(int fd, off_t offset, int whence)
5、close(int fd);
二、缓冲区的进一步认知
三、文件描述符
四、重定向
1、重定向:将原来要写入A的内容不在写入A,而是写入B中
2、重定向的本质
3、加入dup改进minishell 实现可重定向功能
一、系统调用接口介绍
1、open(char* pathname, int flag, mode_t mode)
它具有俩个函数形式?难道是重载?邦邦就是一拳,这不是c++!
俩种函数都对应这其对应的环境下使用,只有一个open函数,只是第三个参数在有些情况需要写出。
返回值:打开成功返回文件的描述符,失败返回-1
第一个参数就是文件路径或者可以在当前代码文件所在文件内自动索引的文件名。
第二个参数:
三者必选其一:(O_RDONLY | O_WRONLY | O_RDWR)
只读(r) 只写(w) 可读可写
可以或上一些的功能:(O_CREAT | O_TRUNC | O_APPEND)
文件不存在时创建文件 截断文件 尾部追加写入
使用这些宏之前的联合使用,可以表示一些特定功能:
比如 w+ :可读可写、文件不存在则创建、打开文件会截断之前文件
那么flag参数就可表示为(O_RDWR | O_CTEATE | O_TAUNC)
open("1.txt", O_RDWR | O_CREAT | O_TRUNC); w+ 格式打开一个文件
比如a+ : 可读可写、文件不存在则创建、尾部追加写入
open("1.txt", O_RDWR | O_CREAT | O_APPEND); a+ 格式打开一个文件
第三个参数:修改文件权限
第三个参数mode只有在当创建文件的时候才需要给出(三位八进制数字,前面加个0)
看上图,发现刚才打开的文件权限中有s这种未知权限,为了处理我们自己创建出来的文件权限问题,就需要给出第三个参数了。
open("1.txt", O_RDWR | O_CREAT | O_APPEND, 0664); 给出664文件权限 110110100 rw-rw-r-- rwx分别代表可读可写可执行
文件掩码:
当涉及文件权限那肯定不能不提系统的默认创建文件掩码
所有的文件创建出来的权限 = 用户给定的文件权限 & (~umask)
比如:想要创建一个文件权限为777的文件
open("1.txt", O_RDWR | O_CREAT | O_APPEND, 0777);
111 111 111 777
& 111 111 101 775(002取反)
= 111 111 101 = 775
而得到的结果确实一个 775权限文件 111 111 101 这就是文件默认掩码的原因
使用umask接口更改文件掩码 umask(0);将当前文件创建文件掩码设置为0,这样用户创建的文件权限就是实际创建的文件权限了。
#include<stdio.h> #include<fcntl.h> // open接口的头文件 #include<sys/stat.h> // umask接口头文件 int main() { umask(0);// 将当前文件默认掩码改为0 open("1.txt", O_RDWR | O_CREAT | O_APPEND, 0777); return 0; }
2、ssize_t write(int fd,char* buf, size_t len)
fd 之前打开文件返回的操作句柄——文件描述符
buf 待写入文件的数据的存储位置
len 要写入的数据长度(以字节为单位)
成功返回实际写入的数据长度,失败返回-1;
3、ssize_t read(int fd,char* buf, size_t len)
fd 打开文件返回的操作句柄——文件描述符
buf 从文件中读取出来数据的存放位置
len 要读取的数据长度
成功返回实际读取的数据长度,0表示读取到了文件末尾; 读取失败返回-1;
4、off_t lseek(int fd, off_t offset, int whence)
fd:文件描述符
offset:偏移量
whence:偏移起始位置 SEEK_SET、SEEK_CUR、SEEK_END
返回值:当前跳转后,读写位置相对于文件起始位置的偏移量(接口的一种另类用法,跳转到末尾,通过返回值确定文件大小)
5、close(int fd);
完成一段简单的程序,将一段数据写入文件,然后将文件指针移动到起始位置之后,在讲文件中的内容读入到一个新的数组中去
#include<stdio.h> #include<unistd.h> // write、read、close、lseek头文件 #include<fcntl.h> // open头文件 #include<string.h> #include<sys/stat.h> // umask头文件 int main() { umask(0);//设置为0,那么当前文件下创建文件权限掩码即为0,即用户所给应用权限即为实际权限 int fd = open("1.txt",O_RDWR | O_CREAT | O_APPEND, 0777); if(fd < 0){ perror("open error"); close(fd); return -1; } //将数据data写入文件中 char* data = "流浪地球2上映了,我还没有看!!!\n"; ssize_t ret = write(fd, data, strlen(data)); if(ret < 0){ perror("write error"); close(fd); return -1; } //将文件中的数据读取到buf数组中 //可是这时的文件指针已经移动到末尾位置了,所以需要将文件指针移动到起始位置 lseek(fd, 0, SEEK_SET); char buf[1024]={0}; ret = read(fd, buf, 1023); if(ret < 0){ perror("read error"); close(fd); return -1; } printf("%s",buf); close(fd); }
二、缓冲区的进一步认知
之前一直说库函数就是对系统调用接口的进一步封装,现在对于文件操作的系统调用接口我也学会了,上一篇博文的对于库函数的描述我也看的差不多了,现在就来对比一下这二者之间的关系把
#include<stdio.h>
#include<unistd.h>
#include<unistd.h>
int main()
{
printf("first");
fwrite("second.", 1, 7, stdout);
fprintf(stdout, "third.");
write(1, "write", 5);
return 0;
}
上面几处打印全部都是为了给标准输出(也就是显示屏终端)上进行打印的代码,前三个都是库函数,而最后一个write是系统调用接口,然而打印结果为:
发现最后一个执行的write却第一个进行打印
改动一下代码,给每一次打印后面加入一个\n来刷新缓冲区,
#include<stdio.h>
#include<unistd.h>
#include<unistd.h>
int main()
{
printf("first\n");
fwrite("second.\n", 1, 8, stdout);
fprintf(stdout, "third.\n");
write(1, "write\n", 6);
return 0;
}
结果发现就是按照次序依次进行打印!!!
原因就是系统调用接口没有缓冲区,当系统调用接口获取到一段数据时,他会不假思索的直接将数据从内核发送到磁盘上,然而资源的处理速度从内核到磁盘一路都是呈现递减状态,就好比内核那边已经开始造火箭了,而磁盘这边还在开着学步车走路,俩者之间的数据传输效率根本没法比,那我为了你这一小块数据我就大费周折的把数据从内核开始运输,这显然降低了效率。
那么库函数在对系统调用接口进行封装的时候就考虑到这一点,于是就引入了缓冲区这个概念,它的作用就是在内存往磁盘传输数据的时候,不会为了你一点点数据来了我就开始传输,而是在内存上开辟一块属于该文件的内存缓冲区,把内存向磁盘方向传输的数据统一先保存到这块缓冲区中,等这块缓冲区满了、或者文件关闭了不在写入数据就把这块缓冲区的数据送往硬盘,这样做就直接减少了内存与磁盘之间的数据传输次数,也就明显的提高了操作系统处理效率。
什么时候会清空缓冲区?
① 缓冲区满了
② 刷新缓冲区(\n、fflush)
③ 文件关闭 或者 程序退出
(三种退出方式main中return、exit、_exit(_exit不执行刷新缓冲区操作))
三、文件描述符
对于这么文件管理的这么多系统调用接口他们之前上下串通的一个重要的线就是这个文件描述符,这个操作句柄,这个int类型的fd到底是个啥呢?
当操作系统打开一个文件的时候,就会把这个文件的相关信息存放到一个struct file结构体中,每次打开一个文件都会创建其对应的struct file结构体,那么就需要对这么些个结构体进行一个规划整合,那么就用一个数组来保存这些文件描述结构体。
文件描述符就是用来记录存放的数组位置的信息,所以说这个小整数也就是文件描述信息存放在数组中的下标。
操作系统通过这个下标来访问文件描述信息存储的位置,通过访问这个文件描述信息从而掌握文件。
打印一下这个fd:
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd = open("1.txt",O_RDWR | O_CREAT | O_TRUNC, 0664);
if(fd < 0){
perror("open error");
return -1;
}
printf("fd = %d\n",fd);
close(fd);
return 0;
}
结果为一个3,之前说过,当Linux打开一个文件时,会默认打开三个文件分别是
0--标准输入 1--标准输出 2--标准错误
那么如果在打开一个文件,那么它所对应的文件描述符(数组下标)也就是下一个3了。
并且Linux满足最小下标分配原则,新打开的文件就会在当前file_array中找到一个最小的合适的下标。
int main()
{
int fd1 = open("1.txt",O_RDWR | O_CREAT | O_TRUNC, 0664);
int fd2 = open("2.txt",O_RDWR | O_CREAT | O_TRUNC, 0664);
int fd3 = open("3.txt",O_RDWR | O_CREAT | O_TRUNC, 0664);
printf("fd1 = %d\n",fd1); fd1 = 3
printf("fd2 = %d\n",fd2); fd2 = 4
printf("fd3 = %d\n",fd3); fd3 = 5
close(fd1);
close(fd2);
close(fd3);
return 0;
}
改变一下文件1的关闭时间:
int main()
{
int fd1 = open("1.txt",O_RDWR | O_CREAT | O_TRUNC, 0664);
int fd2 = open("2.txt",O_RDWR | O_CREAT | O_TRUNC, 0664);
printf("fd1 = %d\n",fd1); fd1 = 3
close(fd1); 此时关闭文件1
int fd3 = open("3.txt",O_RDWR | O_CREAT | O_TRUNC, 0664);
printf("fd2 = %d\n",fd2); fd2 = 4
printf("fd3 = %d\n",fd3); fd3 = 3
close(fd2);
close(fd3);
return 0;
}
关闭标准输入文件
#include<stdio.h> #include<unistd.h> #include<fcntl.h> int main() { close(0); int fd = open("1.txt",O_RDWR); printf("fd = %d\n",fd); fd = 0 close(fd); return 0; }
关闭标准输出文件
int main() { close(1); //关闭标准输出文件 int fd = open("1.txt",O_RDWR); printf("fd = %d\n",fd); close(fd); return 0; }
发现程序运行没有结果!!!
想一想终端上的运行结果是怎么来的?当然是靠我们的stdout标准输出来打印到终端显示上的,现在把终端显示文件关闭了,肯定就没有数据了,那么数据去哪儿了呢?
写到了1.txt中去了嘛?查看一手
仍然没有,哪这个printf的东西打印到哪里了呢?
数据写入磁盘前都是先放入缓冲区中,然后printf的下一步就是关闭文件,那么1.txt文件被关闭了(由于close是系统调用接口,没有缓冲区这个概念,所以它不进行刷新操作),也就写不进去了,最后return退出的时候刷新缓冲区,但是这时候刷新了也没用了,因为printf只与stdout对接,stdout第一步就被关闭了。
所以可以在文件关闭前进行一次刷新即可将缓冲区内容写入到1.txt文件中了。
int main() { close(1); //关闭标准输出文件 int fd = open("1.txt",O_RDWR); printf("fd = %d\n",fd); fflush(stdout); close(fd); return 0; }
四、重定向
1、重定向:将原来要写入A的内容不在写入A,而是写入B中
ls > a.txt 标准输出重定向,将原来打印在终端的内容不再打印,而是写入a.txt中
> 清空重定向,清空原有内容,写入新的内容
>> 追加重定向 新内容追加到文本原有内容末尾
&1 就代表着当前标准输出方向
2、重定向的本质
文件描述符本质就是一个数组下标,通过这个下标访问到文件的描述信息存储,从而通过这个文件描述信息来操作文件。
而重定向的本质就是将一个文件描述符对应信息地址,替换为另一个文件的描述
就比如上面的代码,把1号标准输出文件关闭掉,然后重新打开一个新的文件,就将本来要执行在终端输出窗口的操作执行在了新的文件中,这就是重定向。
为了更方便的控制这些文件的处理,引入了更加丝滑的接口
int dup2 (int oldfd,int newfd);(把new重定向到old)newfd > oldfd
让newfd的位置,保存oldfd对应的位置信息,把本来要进行在newfd上的操作,替换为old文件
具体步骤就是对 在newfd文件内部想要引入oldfd文件,则关闭newfd接着打开oldfd,dup2就是对这一操作的封装实现,它会妥善处理好文件重定向时,文件替换的安全关闭问题。
3、加入dup改进minishell 实现可重定向功能
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>
#include<fcntl.h>
int main()
{
while(1)
{
printf("【minisheill】$ ");
fflush(stdout);
char cmd[1024] = {0};
gets(cmd);
int argc = 0;
char *argv[1024] = {0};
argv[argc++] = strtok(cmd, " ");
while((argv[argc] = strtok(NULL, " ")) != NULL)
{
argc++;
}
// 进行判断是否命令中存在重定向命令
int flag_redirect = 0;
char *file_redirect = NULL;
for(int i = 0; i < argc; i++)
{
if(strcmp(argv[i], ">")==0)
{
flag_redirect = 1; //如果标志位为1 则为清空重定向
// 因为如果含有重定向命令,则即为这样的结构
: ls -l -a > text.txt
argv[i] = NULL; // 这里置空一是为了保证argv中只包含命令
置空之后后续的重定向符号就没了,文件名也没了
// 二是为了后续进行替换的时候参数最后一个必须满足NULL的要求.
file_redirect = argv[i+1]; // argv[i+1] 为文件名
break;
}
else if(strcmp(argv[i], ">>")==0) // 标志位为2 则为追加重定向
{
flag_redirect = 2;
argv[i] = NULL;
file_redirect = argv[i+1];
break;
}
}
pid_t child_pid = fork();
if(child_pid < 0){
perror("fork error");
return -1;
}else if(child_pid == 0)
{
if(flag_redirect == 1)
{
int fd = open(file_redirect, O_RDWR | O_TRUNC | O_CREAT, 0664);
dup2(fd, 1); // 将终端显示的标准输出重定向到fd文件
}else if(flag_redirect == 2)
{
int fd = open(file_redirect, O_RDWR | O_CREAT | O_APPEND, 0664);
dup2(fd, 1);
}
execvp(argv[0], argv);
perror("execvp error");
exit(0);
}
wait(NULL);
}
return 0;
}