目录
C语言文件操作
fprintf编辑
Linux下的文件操作(文件的系统调用接口)
open
open的第三个参数
open的第二个参数
write
read
文件描述符fd
进程与被打开文件的关系(理解的关键)
见见猪跑
fd文件描述符的分配规则
结论
重定向
输入重定向原理
输出重定向原理
dup2
dup2函数使用
将重定向功能添加到myshell中
缓冲区的理解
举例引出缓冲区概念
生活相关
计算机相关
现象解释
缓冲区的刷新策略
模拟实现C语言中的fopen、fwrite、fclose、fflush
Makefile
myStdio.c
myStdio.h
main.c
C语言文件操作
#include <stdio.h>
#include <string.h>
#define FILE_NAME "log.txt"
// C语言操作
int main()
{
FILE *fp = fopen(FILE_NAME, "w"); // r,w r+(读写,不存在出错),w+(读写,不存在创建)
// FILE *fp=fopen(FILE_NAME,"r");//r,w r+(读写,不存在出错),w+(读写,不存在创建)
// FILE *fp=fopen(FILE_NAME,"a");//r,w r+(读写,不存在出错),w+(读写,不存在创建)
// a+ 追加
if (NULL == fp)
{
perror("fopen"); //
return 1;
}
// // char buffer[64];
// // //fgets是会给我们的字符串自动添加一个\0的 因此我们少传一个以至于让fegts有位置放\0
// // while (fgets(buffer, sizeof(buffer) - 1, fp) != NULL)
// // {
// // buffer[strlen(buffer)-1] = 0;//去掉\n
// // puts(buffer);
// // }
// //fprintf:将指定(信息)字符串输出到指定文件中
// int cnt=5;
// while(cnt)
// {
// fprintf(fp,"%s:%d\n","hello mwb",cnt--);
// }
// 文件关闭
fclose(fp);
return 0;
}
fprintf
Linux下的文件操作(文件的系统调用接口)
open
当我们打开一个不存在的文件时,如果没有对其设置具体权限,所产生的文件权限将会混乱。
open的第三个参数
当我们手动设置好权限后,就不会出现上图出现的情况了。
还可以手动消除系统中默认的文件掩码
umask(0);
open的第二个参数
使用位的或运算,对应需要以什么方式打开文件。
根据所需进行或运算。
write
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);// 追加
// int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);//清空原文件内容
// int fd = open(FILE_NAME, O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
int cnt = 5;
char outBuffer[64];
while (cnt)
{
sprintf(outBuffer, "%s:%d\n", "hellomwb", cnt--);
write(fd, outBuffer, strlen(outBuffer));//向文件写入string的时候,要不要+1
}
close(fd);
}
read
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME, O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
char buffer[1024];
ssize_t num = read(fd, buffer, sizeof(buffer) - 1);
if (num > 0)
{
buffer[num] = 0;//0,'\0',NULL 都是0
}
printf("%s", buffer);
close(fd);
}
文件描述符fd
进程与被打开文件的关系(理解的关键)
一个进程可以打开很多文件,操作系统中一定有很多被打开的文件操作系统为了管理对应的打开文件必定要为文件创建对应的内核数据结构标识文件struct file{}。里面包含了文件的大部分属性。
task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
见见猪跑
分别打开5个不同的文件,并且打印他们的文件描述符。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define FILE_NAME(number) "log.txt" #number
int main()
{
int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd: %d\n", fd0);
printf("fd: %d\n", fd1);
printf("fd: %d\n", fd2);
printf("fd: %d\n", fd3);
printf("fd: %d\n", fd4);
close(fd0);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
}
我们能发现他们是连续的小整数:3 4 5 6 7。
1、为什么他们不是从0开始的?
因为0,1,2已经被占用了。
打印标准输入,标准输出,标准错误对应的文件描述符:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
int main()
{
printf("stdin->fd: %d\n",stdin->_fileno);
printf("stdout->fd: %d\n",stdout->_fileno);
printf("stderr->fd: %d\n",stderr->_fileno);
}
2、为什么他们是连续的小整数?
他们其实是一个指针数组 struct file* fd_array[]的数组下标,这个结构体被存在task_struct里面一个指针指向。
3、C语言文件操作函数与Linux下的文件操作函数的关系是什么?
不仅C语言,C++、java、Python等等编程语言,只要在Linux下使用。他们其实都是封装了Linux下的系统调用接口。
跟操作系统层面的这张图是对应的,其他编程语言中的文件操作函数就在用户操作接口这一层。
fd文件描述符的分配规则
当我们把标准输入(fd=0),标准错误(fd=2)关闭的话会出现什么样的结果呢?(先不关闭1,因为1为标准输出,当我们把1关闭的时候观察不到输出的结果了)
如图所示,当我们关闭0的时候,当我们再试图用open打开一个文件并且打印它的文件描述符的时候,它居然变成了0。
然后我们再尝试着关闭2试一下。
同理,我们打开新文件的描述符变成了2。
当然,如果我们关闭1的话是观察不到结果的。
结论
我们文件描述符分配的规则是使用未被占用的最小的那个fd。
重定向
重定向本质就是让本应该输出到一个文件的数据重定向输出到另一个文件中。
输入重定向原理
比如我们需要实现一个让本该输出到显示器的文件数据输出到我们指定的文件当中,那我们就可以在打开一个文件之前把标准输出(fd=1)给关闭,那么我们该文件分配到的就是1的文件描述符。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
const char *msg = "hello mwb\n";
write(1, msg, strlen(msg));
fflush(stdout);
close(fd);
return 0;
}
原理:
输出重定向原理
同理,我们想要把从键盘输入的数据从文件中读取,就可以关闭标准输入(fd=0),然后再打开一个文件,在文件里面对标准输入进行写入。(其实写入的就是文件里面的内容)。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
umask(0);
int fd = open("log.txt", O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
char line[64];
while (1)
{
printf("> ");
if(fgets(line, sizeof(line), stdin)==NULL) break;//stdin里面读取
printf("%s",line);
}
close(fd);
return 0;
}
dup2
要完成重定向我们只需进行fd_array数组当中元素的拷贝即可。
dup2函数使用
也就是说dup2后看似我们使用的是newfd,其实使用的是oldfd,也就是说保留了oldfd。
这里其实很不好理解,我的简单理解就是,让我们使用oldfd的时候其实使用的是newfd的功能。
举例说明:将本该输出到显示屏上的数据显示到文件里面。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
printf("fd: %d\n", fd);
fprintf(stdout, "fd: %d\n", fd);
close(fd);
return 0;
}
从输出结果上我们发现,其实dup2和我们之前使用先关闭标准输出(fd=1)是有所不同的,当前文件的fd还是3,但是其实fd对应数组中的内容已经拷贝到1所在位置了,并且完成了重定向的操作。
将重定向功能添加到myshell中
配有详细注释,有意者可看。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#include <ctype.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
// 先在此处定义,然后while循环中将每次都对这两个变量重新赋初值
// 重定向的方式
int redirType = NONE_REDIR;
// 需要重定向的文件
char *redirFile = NULL;
//start就是重定向符号后面的字符串
#define trimSpace(start) \
do \
{ \
while (isspace(*start)) \
start++; \
} while (0)
char lineCommand[NUM];
char *myargv[OPT_NUM];
int lastCode = 0;
int lastSig = 0;
//检查我们输入的命令中是否包含'>' '<' '>>' 其实也就是是否需要进行重定向操作
void commandCheck(char *commands)
{
//字符串为空直接终止进程报错
assert(commands);
// 从左到右扫描
// 设置两个指针进行遍历操作(其实本质还是找是否有'>' '<' '>>')
char *start = commands;
char *end = commands + strlen(commands);
//便利完成即可退出
while (start < end)
{
// 根据我们遍历到的重定向符号进行相应的操作
if (*start == '>')
{
*start = '\0';
start++;
//若一下if成立则说明是追加重定向'>>'
if (*start == '>')
{
// 修改对应的宏来标记对应的重定向操作
redirType = APPEND_REDIR;
//继续遍历操作
start++;
}
else
{
// 修改对应的宏来标记对应的重定向操作
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("用户名@主机名 当前路径# ");
// 刷新缓冲区,将printf中打印的值给立即输出出来
fflush(stdout);
接受用户输入的字符串
char *s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
assert(s != NULL);
(void)s; // 为使shell不报错,进行强转去掉也无妨
将我们输入的字符串进行分割
// 目的是将我们输入指令后敲的那个回车给去掉
lineCommand[strlen(lineCommand) - 1] = 0;
// printf("test: %s\n",lineCommand);
//"ls -a -l -i" -> "ls" "-a" "-l" "-i" -> l -> n
// 字符串分割之前 要做检查
// 在我们所输入字符串里面去检查是否有 '>' '>>' '<'
commandCheck(lineCommand);
// 字符串切割
myargv[0] = strtok(lineCommand, " ");
int i = 1;
判断是否为ls命令,如果为ls命令 加上颜 色
if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
{
myargv[i++] = (char *)"--color=auto";
}
// 如果没有子串了,strtok->NULL,myargv[end]=NULL
// 将切割后的子串一个个赋值给myargv 当切割结束刚好返回NULL
// while判断条件中先赋值再判断
while (myargv[i++] = strtok(NULL, " "))
;
如果是cd命令,不需要创建子进程,让shell自己执行对应的命令
// 本质就是执行系统接口
// 像这种不需要让我们子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
{
if (myargv[1] != NULL)
chdir(myargv[1]); // chdir可以直接改变当前进程目录
continue; // 直接退出此次while循环,已经不需要再使用子进程了
}
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 DEBUG
for (int i = 0; myargv[i]; i++)
{
printf("myargv[%d]: %s\n", i, myargv[i]);
}
#endif
// 执行命令
// 使用fork创建子进程来完成我们需要完成的进程程序替换
pid_t id = fork();
// 如果id==-1 直接终止程序
assert(id != -1);
// 如果id==0则说明是子进程 接下来进行子进程内的进程程序替换
if (id == 0)
{
// 因为命令是子进程执行的,真正重定向的工作一定是子进程来完成的
// 如何重定向,是父进程要给子进程提供信息的
// 这里重定向会影响父进程吗?
// 不会,因为进程具有独立性,这里对文件的操作父进程中fd不会改变。
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 APPEND_REDIR:
case OUTPUT_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;
}
// 执行程序替换的时候,会不会影响曾经进程打开的重定向文件
// 不会 程序替换是在磁盘与物理地址之间进行的操作
// 进程打开的重定向文件是在进程PCB中的操作,互不干涉
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);
}
}
父子进程共有一块文件区域,由于文件区域不属于进程,因此不需要给子进程拷贝一份儿,至于他们指向文件区域如何后续操作,这里需要我们理解写时拷贝!
缓冲区的理解
举例引出缓冲区概念
生活相关
缓冲区类似与我们的外卖寄存柜,当外卖小哥给我们送外卖的时候,我们刚好在忙,外卖小哥不用当时就给我们交互。他可以把我们的外卖存放到寄存柜里面,然后等我们有空了就可以去拿,这样既不影响我们的正常工作,也给外卖员节省了很多时间。
计算机相关
我们之前讲过,CPU的计算速度(纳秒级别)是极快的,然而磁盘的读取速度(毫秒级别)相对于CPU来说非常慢,如果CPU什么都不干把时间都浪费到去等磁盘将数据读取到内存中,那岂不是效率极低,因此就引入了缓冲区的概念,在内核空间里面有一块内核缓冲区,可供我们的磁盘将数据加载到内存的时候先存放到内核缓冲区中,等CPU来使用。
现象解释
以上代码如果直接执行,我们发现是四条打印内容,可是当我们重定向到log.txt中的时候,log.txt中的内容却与之前不同。我们能很好的观察到C语言的打印接口重复打印了两次,然而系统调用接口只打印了一次。
原因就是C语言缓冲区的存在,当我们fork之后会创建子进程,然而此时我们使用>将所打印的内容重定向到文件log.txt中的时候。会使用全缓冲来进行缓冲区的刷新,全缓冲:等缓冲区满或者关闭fd的时候会刷新缓冲区,然而父子进程会先后退出,然后就把我们文件的内容多拷贝了一份,先后退出进行缓冲区的刷新,造成了打印两份的操作。然而write是系统调用接口,不存在使用C语言层面的缓冲区的概念因此只打印一份。
缓冲区的刷新策略
1、无缓冲(没有缓冲区,对需要写入或者读取数据直接操作)
2、行缓冲(遇到'\n'进行刷新,一行刷新一次)文章来源:https://www.toymoban.com/news/detail-456569.html
3、全缓冲(缓冲区满了才会进行刷新)文章来源地址https://www.toymoban.com/news/detail-456569.html
模拟实现C语言中的fopen、fwrite、fclose、fflush
Makefile
main:main.c myStdio.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f main
myStdio.c
#include "myStdio.h"
FILE_ *fopen_(const char *path_name, const char *mode)
{
int flags = 0;
int defaultMode = 0666;
if (strcmp(mode, "r") == 0)
{
flags |= O_RDONLY;
}
else if (strcmp(mode, "w") == 0)
{
flags |= (O_WRONLY | O_CREAT | O_TRUNC);
}
else if (strcmp(mode, "a") == 0)
{
flags |= (O_WRONLY | O_CREAT | O_APPEND);
}
else
{
// TODO
}
int fd = 0;
if (flags & O_RDONLY)
fd = open(path_name, flags);
else
fd = open(path_name, flags, defaultMode);
if (fd < 0)
{
const char *err = strerror(errno);
write(2, err, strlen(err));
return NULL; // 为什么打开文件失败会返回NULL
}
FILE_ *fp = (FILE_ *)malloc(sizeof(FILE_)); // 申请堆空间
assert(fp);
fp->flags = SYNC_LINE; // 默认设置成行刷新
fp->fileno = fd;
fp->cap = SIZE;
fp->size = 0;
memset(fp->buffer, 0, SIZE);
return fp; // 为什么你们打开一个文件,就会返回一个 FILE* 指针
}
void fwrite_(const void *ptr, int num, FILE_ *fp)
{
// 1、写入到缓冲区中
memcpy(fp->buffer + fp->size, ptr, num);
//fp->buffer指向缓冲区的开头+fp->size指向size后,已经使用了size
//所以接下里的文件应该放在size后面
//memcpy的意思是将从ptr开始num个数据复制到从fp-buffer+fp->size的位置之后
// 这里我们不考虑缓冲区溢出的问题
fp->size += num;
// 2、判断是否刷新
if (fp->flags & SYNC_NOW)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0; // 清空缓冲区
}
else if (fp->flags & SYNC_FULL)
{
if (fp->size == fp->cap)
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
else if (fp->flags & SYNC_LINE)
{
// abcd\nef不考虑
if (fp->buffer[fp->size - 1] == '\n')
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
else
{
}
}
void fflush_(FILE_ *fp)
{
if (fp->size > 0)
write(fp->fileno, fp->buffer, fp->size);
fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!
fp->size=0;
}
void fclose_(FILE_ *fp)
{
fflush_(fp);
close(fp->fileno);
}
myStdio.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include<stdlib.h>
#include<assert.h>
#define SIZE 1024
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4
typedef struct _FILE
{
int flags;// 刷新方式
int fileno;
int cap; //buffer的总容量
int size;//buffer当前的使用量
char buffer[SIZE];
} FILE_;
FILE_ *fopen_(const char *pathname, const char *mode);
void fwrite_(const void *ptr, int num, FILE_ *fp);
void fclose_(FILE_ *fp);
void fflush_(FILE_ *fp);
main.c
#include "myStdio.h"
#include <stdio.h>
int main()
{
FILE_ *fp = fopen_("./log.txt", "w");
if (fp == NULL)
{
return 1;
}
int cnt = 10;
const char *msg = "hello mwb ";
while (1)
{
fwrite_(msg, strlen(msg), fp);
fflush_(fp);
sleep(1);
printf("count: %d\n", cnt);
// if (cnt == 5)
// fflush_(fp);
cnt--;
if (cnt == 0)
break;
}
fclose_(fp);
return 0;
}
到了这里,关于Linux——基础IO的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!