进程打开文件

本文详细解析了C语言中操作文件的函数、系统调用,探讨了进程打开文件的机制,包括文件描述符、structfile内核对象、fd的分配规则和重定向,以及标准错误流的应用。并通过实例展示了如何在自定义命令行解释器中实现重定向功能。

目录

一、预备知识

二、操作文件函数

三、操作文件系统调用

四、理解进程打开文件

函数 vs 系统调用

open的返回值 fd

如何理解一切皆文件?

理解struct file 内核对象

fd的分配规则

重定向

标准错误流(2号文件描述符)

五、缓冲区

六、封装实现C库函数


一、预备知识

1. 文件 = 内容 + 属性, 对文件的操作无非就是对内容操作和对属性操作

2. 内容是数据,属性也是数据,所以磁盘存储文件,既要存储文件内容,也要存储文件属性

3. C语言有打开访问关闭文件的库函数,而只有当程序运行起来时,cpu才会读取C语言代码,执行对文件的操作,所以本质是进程访问了文件!

4. 文件默认是在磁盘上的,由于cpu在数据层面只和内存交互,因此除了我们自己写的可执行程序要加载到内存,打开文件后文件的属性和内容也要被加载到内存,由于涉及到访问底层硬件磁盘,这个工作依旧是OS完成的!

5. 一个进程可以打开一个文件,一个进程也可以打开多个文件,多个进程也可以打开多个文件

6. 既然多个进程会打开多个文件,那么OS也要对多个文件进行管理:先描述,再组织!

7. 文件无非分为打开了的文件和没有被打开的文件,所以对文件的学习就分为这两部分

文件打开前: 磁盘文件    文件打开后: 将文件加载到了内存中

8. 研究内存中的文件的本质就是研究进程和被打开文件的关系!

二、操作文件函数

这属于C语言的知识点,我们快速回顾一下,详见 详解C语言文件操作

打开文件,写入文件,关闭文件

新建写与追加写

文件以"w"方式打开,之前的内容会被清空,所以之前讲的 > log.txt 输出重定向就可以清空文件

文件以"a"方式打开,会从文件结尾开始写入, 不会清空文件内容

三、操作文件系统调用

进程打开文件涉及到了访问磁盘,所以必定是借助了OS的能力,而OS不相信任何用户,所以打开文件、读取文件、写入文件的函数底层一定封装了系统调用!

open

参数:

pathname:文件路径+文件名,只有文件名,默认当前路径

● flags:多个标志位的组合,从而决定文件打开方式

● mode:打开文件指定的权限(开始的权限,文件最终权限还要去掉权限掩码)

返回值:

打开成功,返回文件描述符 fd,打开失败,返回-1

下面我们举例说明 标志位 flags 如何使用:

write

参数:

● fd:open的返回值,表示文件描述符

● buf:写入文件的字符串的起始地址

● count:要写入文件的字节数

返回值:

成功写入文件的字节个数

代码演示

上述所传的参数,是从头开始覆盖式地写入,文件内容并不会清空

要想打开文件后,原始内容清空,需要额外传递 O_TRUNC 参数

想要追加式写入,需要传递宏: O_APPEND

四、理解进程打开文件

函数 vs 系统调用

open的返回值 fd

程序启动后加载到内存中,OS要管理进程,创建 task_struct 结构体,cpu 执行进程的代码,也打开文件,文件也要加载到内存中,OS要创建描述文件的结构体,多个进程可能打开多个文件,如何知道哪个进程打开了哪个文件,进程和文件之间如何关联呢?

task_struct 中有一个指针变量,指向了一个结构体对象,结构体对象中有一个成员数组(名称叫做进程文件描述符表),这个数组中就存储了描述文件的结构体对象的地址,于是进程和文件本质还是解耦的,但是可以关联了!

数组的下标就叫做文件描述符!!

所以调用open打开文件时,OS会先创建struct file结构体,然后将该结构体地址填充到文件描述符表的特定位置,然后向上层用户返回数组下标 ,即open的返回值fd!

当调用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 内核对象

文件被打开后要加载到内存,文件=内容+属性,所以 struct file (纯内核数据结构) 中肯定要有一堆描述文件属性的字段,同时 struct file 中有一个指针,指向了一段内存空间,充当文件缓冲区,用于存储文件内容,除此之外,struct file 中也要有一个字段,保存文件读写的方法集(一堆函数指针)

当要读取文件内容时,由于 cpu 在数据层面只和内存进行交互,因此要现将磁盘上的文件内容加载到文件缓冲区才能进行读取,那向文件写入时也要将文件内容加载到文件缓冲区吗?是的!因为覆盖文件内容是写入,修改部分内容也是写入呀!因此写入也要将文件内容加载到文件缓冲区,在内存中完成写入操作后将文件缓冲区内容定期刷新到磁盘上!

我们之前在用C语言完成文件读写操作时都会定义 一个 buffer 缓冲区,这个缓冲区本质就是用户级缓冲区,文件读写的本质就是用户缓冲区和文件缓冲区的数据之间的拷贝!

fd的分配规则

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    close(0); //关闭0号(标准输出流)文件描述符
    // close(2); //关闭2号(标准错误流)文件描述符
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 
    if(fd < 0)
    {
        printf("文件打开失败");
        return 1;
    }
    printf("%d\n", fd); //当关闭了0/2号文件描述符之后, 打印的就是0/2号文件描述符
    return 0;
}

结论:新打开一个文件,对应的fd为文件描述符表中最小的未分配的文件描述符!!!

重定向

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    close(1);  //关闭标准输出流
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 
    if(fd < 0)
    {
        printf("文件打开失败");
        return 1;
    }
    fprintf(stdout, "fd: %d\n", fd);
    fprintf(stdout, "stdout->%d\n", fd);
    fflush(stdout);  //必须加这句话, 否则打印不出来!
    close(fd);
    return 0;
}

现象:先关闭了 1 号文件描述符,然后打开 file.txt 文件,此时我们向 stdout 标准输出 (对应显示器) 写入时发现显示器上并没有内容,而是将内容写入到了 file.txt 文件中!

上述过程 不就是 将原本要写入显示器文件的内容 写入到了 file.txt 文件中,不就是输出重定向吗!

原理:1号文件描述符原本和显示器文件关联,当我们 close(1),本质是将 1号文件描述符和显示器的关联断开,然后又打开 file.txt 文件,根据文件描述符的分配规则,1号文件描述符会和 log.txt 关联,而 fprintf 是将内容输出到 stdout 标准输出流,stdout 底层封装的是 1号文件描述符,OS访问文件只认文件描述符,而此时 1 号文件描述符已经关联的是 log.txt 文件了,因此发生了输出重定向,上层用户并不知道底层"狸猫换太子"的整个过程!

要实现追加重定向只需要把"O_TRUNC"改为"O_APPEND"即可

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    close(1);  //关闭标准输出流
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 
    if(fd < 0)
    {
        printf("文件打开失败");
        return 1;
    }
    fprintf(stdout, "fd: %d\n", fd);
    fprintf(stdout, "stdout->%d\n", fd);
    fflush(stdout); 
    close(fd);
    return 0;
}

输入重定向也是同样的原理,当0号文件描述符被关闭之后,再从stdin中读取数据,此时就变成了从log.txt中读数据了! 本来应该从键盘读取数据变成了从file.txt中读取数据,这就叫做输入重定向!

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    close(0);  //关闭标准输入流
    int fd = open("file.txt", O_RDONLY); 
    if(fd < 0)
    {
        printf("文件打开失败");
        return 1;
    }
    char buffer[1024];
    ssize_t n = fread(buffer, 1, sizeof(buffer), stdin);
    if(n > 0) buffer[n] = '\0';
    printf("%s\n", buffer);
    return 0;
}

系统调用 dup2

上述重定向都要先关闭文件描述符,才能完成重定向工作,但我们发现重定向的本质就是对进程文件描述符数组相应下标内容的覆盖(拷贝),因此要完成重定向可以不用关闭文件描述符,直接使用系统调用即可,但是多个指针指向了同一个文件结构体对象,什么时候释放呢?引用计数!

dup2是系统调用,参数的含义是将oldfd指向的内容拷贝覆盖到newfd指向的内容!

为啥一个进程默认打开 0/1/2 文件描述符?

因为 fork 创建子进程,子进程除了拷贝pcb/地址空间/页表等,文件描述符表也会拷贝一份,本质就是将父进程指向struct file 的指针拷贝了一份,因此只要父进程默认打开了0/1/2文件描述符,子进程也就会打开!因此之前我们写多进程代码时,父子进程向同一个显示器打印内容!

子进程关闭文件描述符,会影响父进程嘛?

不会!struct file 中维护了引用计数字段,只有当引用计数--为0,才会释放打开的文件!

#include <stdio.h>
#include <unistd.h>
int main()
{
    pid_t id = fork();
    if(0 == id)
    {
        close(1);
    }
    while(1)
    {
        printf("haha\n"); //正常打印
    }
    return 0;
}

程序替换会影响重定向嘛?

完全不影响!程序替换并没有创建新进程,只是将内存的代码和数据进行了替换,完全不影响进程对应的文件描述符表,也就不影响重定向功能!

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    dup2(fd, 1);
    execl("/usr/bin/ls", "ls", "-al", NULL);
    return 0;
}

支持重定向功能的自己实现的命令行解释器

上篇博客我们实现了一个命令行解释器bash, 但是不支持命令行级别的重定向功能!理解了重定向之后就可以把该功能加进来了!

//实现一个命令行解释器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.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]; //必须是全局有效的, 否则cd函数退出后, cwd就销毁了, PWD环境变量是查不到的
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(); //cd后面啥都不跟, 就回到家目录
        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
        }

        //int execvp(const char *file, char *const argv[]); 
        execvp(argv[0], argv);
        exit(1);
    }
    else
    {
        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);
        
        //1.1.检查重定向
        checkRedir(usercommand, strlen(usercommand));

        //2.对字符串做切割
        char* argv[SIZE];
        commandSplit(usercommand, argv);
 
        //3.检查命令是否内建命令
        n = doBulidin(argv); //不是内建命令返回0
        if(n) continue; //内建命令由父进程直接执行,不用创建子进程(比如cd, 如果让子进程执行, 切换的是子进程的cwd, 父进程cwd一直不变)
 
        //4.创建子进程执行对应的命令
        execute(argv);
    }
    return 0;
}

标准错误流(2号文件描述符)

#include <iostream>
using namespace std;
int main()
{
    fprintf(stdout, "hello stdout\n");
    fprintf(stderr, "hello stderr\n");
    return 0;
}

stdout 和 stderr 分别是标准输出和标准错误,对应的都是显示器,因此都可以往显示器上输出

当我们进行输出重定向时,本质是将 1号 文件描述符的内容不再和 显示器文件关联,而是和 log.txt 文件关联,因此 cout 会将消息打印到 log.txt 文件中,而 stderr 封装的时 2号文件描述符,依旧对应的是显示器,因此正常向显示器输出

其实 ./a.out > log.txt 是简略的写法,完整的写法是 ./a.out 1>log.txt,1>log.txt 表述将 1号文件描述符和 log.txt 文件关联,因此 ./a.out 的内容会输出到 log.txt 中

上述写法的意思是 现将1号文件描述符和 log.txt 关联,然后将 1号文件描述符的内容拷贝给 2号文件描述符,本质就是系统调用dup2的行为,因此2号文件描述符也就和 log.txt 关联了,因此无论是 stdout 还是 stderr,都会将内容输出到 log.txt 中!

为啥要有标准错误流呢??

有的消息是常规消息,有的消息是错误消息,有了标准错误流,通过重定向,我们就可以将常规消息和错误消息分开,放在两个不同的文件中,方便排查错误信息!!!

C++中的cout和cerr也是同样的道理!

#include <iostream>
using namespace std;
int main()
{
    cout << "this is normal info" << endl;
    cerr << "this is error info" << endl;
    return 0;
}

五、缓冲区

是什么?

缓冲区本质就是一段内存空间,我们之前接触过的所有的缓冲区,都是语言级别的缓冲区!

为什么?

本质是提高效率!为啥能提高效率?

1. 如果没有语言级别的缓冲区,那么用户数据写入到文件中,就要频繁的调用系统调用,系统调用是有成本的,频繁调用系统调用成本很高,而有了缓冲区,就可以累积一定的数据在缓冲区中,再调用系统调用写入即可!

2. 只要写入到语言级别的缓冲区,用户(进程)就认为自己写完了,可以直接返回了,提高了IO接口的调用效率!

怎么做?

缓冲区存在于C语言标准库定义的FILE结构体内部!

#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello world, hello student, hello people");
    sleep(3);
    return 0;
}

先睡眠了3s,再打印出语句,证明了缓冲区的存在!

缓冲区的刷新策略

一般策略:

1. 无缓冲(立即刷新)

2. 行缓冲(行刷新)

3. 全缓冲(缓冲区满了,再刷新) 

两种情况:

1. 强制刷新(比如C语言的 fflush)

2. 进程退出时一般会刷新缓冲区

注: 默认情况下显示器文件一般采用行刷新,磁盘上的文件一般采用全刷新

下面我们通过一段代码以及运行结果深入理解缓冲区:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
    fprintf(stdout, "C: hello fprintf\n");
    printf("C: hello printf\n");
    fputs("C: hello fputs\n", stdout);
    const char* str = "system call: hello write\n";
    write(1, str, strlen(str));
    return 0;
}

显示器文件的缓冲策略是行缓冲,\n本身就是行刷新的一种方式,因此打印的所有内容可以直接一次性全部显示到显示器上;磁盘文件的缓冲策略是全缓冲,也就是缓冲区变大了,所以重定向到磁盘文件的内容会暂时存在缓冲区里面,但进程结束时会刷新缓冲区,因此也能看到内容都输出到了文件中

细节:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
    fprintf(stdout, "C: hello fprintf\n");
    printf("C: hello printf\n");
    fputs("C: hello fputs\n", stdout);
    const char* str = "system call: hello write\n";
    write(1, str, strlen(str));
    fork();
    return 0;
}

现象:在程序结束前我们调用了 fork,直接打印到显示器上是正常的输出结果,但是当我们将程序运行结果 输出重定向到 log.txt 中,发现系统调用只被调用了一次,而所有的C库函数都调了两次

原因:

1. 显示器文件刷新方式是行刷新,且 fork 之前所有的代码都带\n,就是行刷新策略,因此在 fork 之前所有的语句就已经打印到显示器上了!

2. 如果是重定向到 log.txt, 本质是访问了磁盘文件,刷新方式从行缓冲变成了全缓冲!这意味着缓冲区变大,实际写入的数据不足以把缓冲区写满,fork执行时,数据依然在缓冲区中!

3. 而我们发现,无论如何,系统调用只打印了一次,而加了fork之后,重定向到文件中的函数打印了两次,因为这些函数底层封装的都是write系统调用,这就说明目前我们所说的缓冲区和操作系统没有关系,只能和C语言有关系! 我们日常用的最多的缓冲区就是C语言提供的缓冲区!

4. C/C++提供的缓冲区,里面保存的是用户的数据,属于当前进程运行时自己的数据;而如果通过系统调用把数据写入到了OS内部(文件缓冲区),那么数据就不属于用户了!

5. 当进程退出时,要刷新缓冲区,刷新缓冲区也属于写入操作,而fork创建子进程后,父子进程任何一方要对数据写入时,都要发生写时拷贝,所以数据会出现两份!而系统调用只有一份,是因为系统调用是在库之下的,不适用C语言提供的缓冲区,write 直接将数据写入OS,不属于当前进程的数据了,所以不发生写时拷贝,数据

0只有1份!

这样就解释了为啥重定向到 log.txt 文件中 系统调用输出的内容是最先被打印的,因为系统调用是直接将数据写入文件中的,C语言函数输出的数据暂时在缓冲区中,要等进程结束后刷新缓冲区才会被写入到文件中!

上述C语言提供的缓冲区位于FILE结构体内部!!!

六、封装实现C库函数

mystdio.h

#pragma once

#include <stdio.h>
#define SIZE 4096
#define FLUSH_LINE 1 
#define FLUSH_ALL (1 << 1)

typedef struct _myFILE
{
    int fileno;
    char buffer[SIZE]; //缓冲区
    int end; //缓冲区已有空间大小
    int flag; //刷新策略
}myFILE;

extern myFILE* my_fopen(const char *path, const char *mode);
extern int my_fwrite(const char* s, int num, myFILE* stream);
extern int my_fflush(myFILE* stream);
extern int my_fclose(myFILE* stream);

mystdio.c

#include "mystdio.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#define DEL_MODE 0666

myFILE* my_fopen(const char *path, const char *mode)
{
    int fd = 0;
    int flag = 0;
    if(strcmp(mode, "r") == 0)
    {
        flag |= O_RDONLY;
    } 
    else if(strcmp(mode, "w") == 0)
    {
        flag |= (O_CREAT | O_WRONLY | O_TRUNC);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flag |= (O_CREAT | O_WRONLY | O_APPEND);
    }
    else
    {
        //do nothing
    }
    if(flag & O_CREAT)
    {
        fd = open(path, flag, DEL_MODE);
    }
    else
    {
        fd = open(path, flag);
    }
    if(fd < 0) 
    {
        errno = 2;
        return NULL;
    }
    myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
    if(!fp) 
    {
        errno = 2;
        return NULL;
    }
    fp->flag = FLUSH_LINE; //默认行刷新
    fp->end = 0; 
    fp->fileno = fd; 
    return fp;
}

int my_fwrite(const char* s, int num, myFILE* stream)
{
    memcpy(stream->buffer+stream->end, s, num);
    stream->end += num;
    //判断是否需要刷新
    if((stream->flag & FLUSH_LINE) && stream->end > 0 && stream->buffer[stream->end-1] == '\n')
    {
        my_fflush(stream);
    }
    return num;
}

int my_fflush(myFILE* stream)
{
    if(stream->end > 0)
    {
        write(stream->fileno, stream->buffer, stream->end);
        //fsync(stream->fileno); //把数据刷新到内核中
        stream->end = 0;
    }
    return 0;
}

int my_fclose(myFILE* stream)
{
    my_fflush(stream);
    return close(stream->fileno);
}

main.c 

#include "mystdio.h"
#include <string.h>
#include <unistd.h>

int main()
{
    myFILE* fp = my_fopen("./log.txt", "w");
    if(fp == NULL)
    {
        perror("my_fopen");
        return 2;
    }
    // const char* msg = "haha, this is my clib\n";
    const char* msg = "haha, this is my clib";
    int cnt = 20;
    while(cnt--){
        my_fwrite(msg, strlen(msg), fp);
        sleep(1);
    }
    my_fclose(fp);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值