Linux文件(系统)IO(含动静态库的链接操作)


img

Linux文件(系统)IO(含动静态库的链接操作)

1、C语言文件IO操作

  • fopen打开文件,fclose关闭文件

  • fwrite操作(写文件)

fwrite函数从ptr里将nitems个大小为size字节的数据写进定stream

  • hello_w.c文件

    #include <stdio.h>                                                              
    #include <string.h>    
    
    int main(){    
        FILE* pf = fopen("myfile.txt","w");    
        if(!pf){    
            perror("fopen");    
            return 1;    
        }    
    
        const char* msg = "hello fopen\n";    
        int count = 5;    
        while(count--){    
            fwrite(msg,strlen(msg),1,pf);    
        }    
    
        fclose(pf);    
        return 0;    
    }
    

    这里我们将会在此路径得到一个写有5hello fopenmyfile.txt文件,如下gif:

  • fread操作(写文件)

fread 不会在字符串末尾添加 null 终止符(\0),fgets会。

fread函数从定stream里读取nitemssize字节大小的数据存到ptr

  • hello_r.c文件

    #include <stdio.h>
    #include <string.h>
    
    int main(){
        FILE* pf = fopen("myfile.txt","r");
        if(!pf){
            perror("fopen");
            return 1;
        }
    
        char buffer[1024];
    
        while(1){
            //fgets
            // 每次读1个字节,读1024次(文件结束就不读)
            size_t s = fread(buffer,1,sizeof(buffer),pf);
            if(s > 0){
                buffer[s] = 0;
                printf("%s",buffer);
            }
            if(feof(pf)){
                break;
            }
        }
        fclose(pf);
        return 0;
    }
    

    这里我们将会在此路径读到myfile.txt文件,看到打印出来的数据,如下gif:

  • fgets操作(每次读文件的一行))

fgets函数从给定stream中读取size个字节放到str中,当发现换行符、文件结束符或发生错误时,读取将停止。

#include <stdio.h>
#include <string.h>

int main(){
    FILE* pf = fopen("myfile.txt","r");
    if(!pf){
        perror("fopen");
        return 1;
    }

    char buffer[1024];

    const char* msg = "hello xxxxx\n";    

    while(1){    
        char* r = fgets(buffer,sizeof(msg),pf);                                                  
        if(!r)    
            break;    
        printf("%s",buffer);    
    }

    fclose(pf);
    return 0;
}

  • 打开文件的方式:
			 r      打开文本文件,用于读。流被定位于文件的开始。

       r+     打开文本文件,用于读写。流被定位于文件的开始。

       w      将文件长度截断为零,或者创建文本文件,用于写。流被定位于文件的开始。

       w+     打开文件,用于读写。如果文件不存在就创建它,否则将截断它。流被定位于文件的开始。

       a      打开文件,用于追加
              (在文件尾写)。如果文件不存在就创建它。流被定位于文件的末尾。

       a+     打开文件,用于追加
              (在文件尾写)。如果文件不存在就创建它。读文件的初始位置是文件的开始,但是输出总是被追加到文件的末尾。

2、三个数据流stdin、stdout、stderr

  • stdin(标准输入流):我们在C语言常用的scanf函数,其默认就是指定了标准输入流stdin
#include <stdio.h>

int main(){
  int x = 0;
  scanf("%d",&x);
  return 0;
}

fread使用标准输入流:

#include <stdio.h>    
#include <string.h>    
    
int main(){ 
		char buffer[10];    
    // 这里要注意,这里的sizeof(buffer)-1是多少,fread就会去读多少数据,直到读到这么多数据    
    size_t bytesRead = fread(buffer, sizeof(char), sizeof(buffer)-1, stdin);    
    buffer[bytesRead] = '\0';  // 字符串结束符    
    printf("buff:%s",buffer);
    return 0;
}

如果我们想指定输入流,我们可以使用fscanf函数:

#include <stdio.h>    
    
int main(){    
    
    FILE* pf = fopen("myfile","r");    
    if(!pf){    
        perror("fopen error");    
        return 1;    
    }    
    
    int in = 0;    
    fscanf(pf,"%d",&in);    
    printf("%d\n",in);
  	fclose(pf);
    
    return 0;    
}

  • stdout(标准输出流):我们在C语言常用的printf函数,其默认就是指定了标准输入流stdout
#include <stdio.h>

int main(){
  printf("%d\n",2);
  return 0;
}

fwrite使用标准输出流:

#include <stdio.h>    
#include <string.h>                                                                              
    
int main(){    
    
    FILE* pf = fopen("myfile","w");    
    if(!pf){    
        perror("fopen");    
        return 1;    
    }    
    
    const char* msg = "hello fprintf\n";       
    
    fwrite(msg,strlen(msg),1,stdout);
  	fclose(pf);	
    return 0;    
}

如果我们想指定输出流,我们可以使用fprintf函数:

#include <stdio.h>    
    
int main(){    
    
    FILE* pf = fopen("myfile","w");    
    if(!pf){    
        perror("fopen error");    
        return 1;    
    }    
  
    const char* msg = "hello fprintf\n";    
    fprintf(pf,"%s",msg);                                                                       
    fclose(pf);
    return 0;    
}

  • stderr(标准输入流)(后面讲)

3、系统文件IO

3.1、相关系统调用接口的使用

  • 类比C标准库中的函数fopen、fwrite、fread等,我们称之为库函数(权限低)(底层就是使用系统调用接口)。

  • 而open、write、read等属于系统提供的接口,我们称之为系统调用接口(权限高)。

  • 参考下图的lib和系统调用接口

  • open打开文件,close关闭文件

这里注意有两个open系统调用接口,第一个不带第三个参数mode,这个mode就是给创建的文件设置权限的比如0666,就是让这个文件的root、自己、其他人的权限都设置为只能读写,当然,这个还要看权限掩码umask是多少,假如umask是0002这最后文件的权限为0664(前面的博客有讲权限掩码,自行查阅)。

  • 系统调用接口write写文件

write接口试图从buf中取nbyte字节的数据写到到文件描述符fildes指向的空间中

#include <stdio.h>    
#include <fcntl.h>    
#include <unistd.h>    
#include <string.h>    
    
int main(){    
        
    int fd = open("myfile",O_WRONLY|O_TRUNC|O_CREAT,0666);                      
    		 printf("%d\n",fd);// 3
  	 const char* msg = "hello write\n";    
    write(fd,msg,strlen(msg));    
    
    close(fd);    
    return 0;    
}
  • 系统调用接口read读文件

read接口试图从文件描述符fildes指向的空间中读取nbyte字节放到buf

#include <stdio.h>    
#include <fcntl.h>    
#include <unistd.h>    
#include <string.h>    
    
int main(){    
        
    int fd = open("myfile",O_RDONLY);    
        		 printf("%d\n",fd);// 3
    char buff[64];        
    const char* msg = "hello write\n";    
        
    read(fd,buff,strlen(msg));    
    printf("%s",buff);
    
    close(fd);    
    return 0;    
}

3.2、文件描述符fd

  • 经过上面对文件myfile的写读,我们知道这个open的返回值fd是3,那么为什么返回值是3?我们再打开几个文件试试它们的文件描述符到底是多少:

    #include <stdio.h>    
    #include <fcntl.h>    
    #include <unistd.h>    
    #include <string.h>    
        
    int main(){
    		int fd1 = open("myfile1",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd2 = open("myfile2",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd3 = open("myfile3",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd4 = open("myfile4",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd5 = open("myfile5",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        
        printf("%d\n",fd1);    
        printf("%d\n",fd2);    
        printf("%d\n",fd3);                                                         
        printf("%d\n",fd4);    
        printf("%d\n",fd5);
     
      	//close省时间不写了
      	return 0;
    }
    

    我们得出结果是:

    这像是什么?是不是数组的下标!那么问题又来了,前面的0,1,2去哪了?这时我们想到还有三个数据流,分别是stdin、stdout、stderr。我们继续来验证一下:

    #include <stdio.h>    
    #include <fcntl.h>    
    #include <unistd.h>    
    #include <string.h>    
        
    int main(){
    		int fd1 = open("myfile1",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd2 = open("myfile2",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd3 = open("myfile3",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd4 = open("myfile4",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        int fd5 = open("myfile5",O_WRONLY|O_TRUNC|O_CREAT,0666);    
        
        printf("%d\n",stdin->_fileno);    
        printf("%d\n",stdout->_fileno);    
        printf("%d\n",stderr->_fileno);    
        printf("%d\n",fd1);    
        printf("%d\n",fd2);                                                         
        printf("%d\n",fd3);    
        printf("%d\n",fd4);    
        printf("%d\n",fd5);
     
      	//close省时间不写了
      	return 0;
    }
    

    这个打印出来的结果是:

    因此验证了我们的猜想!

  • 而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。如下图:


3.3、文件描述符的分配规则

  • 来看下列代码:
#include <stdio.h>    
#include <fcntl.h>    
#include <unistd.h>    
                                                                                
int main(){    
    
    int fd = open("myfile",O_RDONLY);    
    
    if(fd < 0){    
        perror("open");    
        return 1;    
    }    
    printf("%d\n",fd);    
    
    close(fd);    
    return 0;    
}

输出是3,关闭0或2试试(这里不能关闭1,因为1是标准输出流,关闭了我们就看不到输出结果,想看到输出结果我们需要进行输出重定向,下面会讲)

#include <stdio.h>    
#include <fcntl.h>    
#include <unistd.h>    
                                                                                
int main(){    
    
  	close(0);
  	//close(2);
    int fd = open("myfile",O_RDONLY);    
    
    if(fd < 0){    
        perror("open");    
        return 1;    
    }    
    printf("%d\n",fd);    
    
    close(fd);    
    return 0;    
}

输出是0或者2。说明了什么?说明文件描述符的分配规则是在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。


3.3、重定向

  • 重定向分为输入重定向和输出重定向

  • 针对上面关闭1的情况,关闭1后看不到输出结果了,那么我们可以进行输出重定向,看下面代码:

    #include <stdio.h>    
    #include <fcntl.h>    
    #include <unistd.h>    
                                                                                    
    int main(){    
        
      	close(1);
      	
        int fd = open("myfile",O_WRONLY|O_CREAT,0666);    
        
        if(fd < 0){    
            perror("open");    
            return 1;    
        }    
        printf("%d\n",fd);    
        
      	fflush(stdout);// 	刷新缓冲区
        close(fd);    
        return 0;    
    }
    

    我们发现,本来应该输出到显示器上的数据,输出到了myfile文件中!这种现象叫做输出重定向。输入重定向类似(从myfile文件中读取数据)。

    常见的重定向有:>, >>, <

  • 那么重定向的本质是什么?看下图:

    也就是将file*的指针重新指向新的文件。

  • 除了使用关闭标准输入输出流的方法实现重定向功能,还可以使用dup2系统调用。

    dup2系统调用,fildes2文件描述符是指定的,fildes是需要重定向的文件描述符。

    使用dup2系统调用代码:

    #include <stdio.h>    
    #include <fcntl.h>    
    #include <unistd.h>    
       
    int main(){    
       
       int fd = open("myfile",O_RDWR|O_CREAT,0666);                                   
       if(fd < 0){    
           perror("open");    
           return 1;    
       }    
       
       close(1);    
       dup2(fd,1);    
       
       printf("hello dup2\n");    
       fflush(stdout);    
       return 0;    
    }
    

    #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;
    }
    

3.4、自制shell加入重定向功能

  • myshell.c文件

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <fcntl.h>
    #include <sys/stat.h>
                                        
    #define SIZE 1024
    #define MAX_LEN 128
    #define SEP " "
                                        
    // 下面的都和重定向有关
    #define NoneRedir  -1
    #define StdinRedir  0
    #define StdoutRedir 1
    #define AppendRedir 2
                                        
    //#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)
                                        
    int redir_type = NoneRedir;
    char *filename = NULL;
                                        
    char* argv[MAX_LEN];//命令行字符串数组
    char pwd[SIZE];
    char envtemp[SIZE];
    int lastcode = 0;//退出码
                                        
    const char* HostName(){
        char* hostname = getenv("HOSTNAME");
        if(hostname) return hostname;
        else return "None";
    }
                                        
    const char* UserName(){
        char* hostname = getenv("USER");
        if(hostname) return hostname;
        else return "None";
    }
                                        
    const char* CurrentWorkDir(){
        char* hostname = getenv("PWD");
        if(hostname) return hostname;
        else return "None";
    }
                                        
    char* Home(){
        char* hostname = getenv("HOME");
        if(hostname) return hostname;
        else return "None"; 
    }
                                        
    int Interactive(char* commandline, int size){
        printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir());
        fgets(commandline,SIZE,stdin);
        commandline[strlen(commandline)-1] = '\0';
                                        
        return strlen(commandline);//空串返回0
    }
                                        
    void Check_redir(char in[])
    {
         // ls -a -l
        // ls -a -l > log.txt
        // ls -a -l >> log.txt
        // cat < log.txt
        redir_type = NoneRedir;
        filename = NULL;
        int pos = strlen(in) - 1;
        while( pos >= 0 )
        {
            if(in[pos] == '>')
            {
                if(in[pos-1] == '>')
                {
                    redir_type = AppendRedir;
                    in[pos-1] = 0;
                    pos++;
                    //IgnSpace(in, pos);
                                        
                    while(in[pos] == ' '){
                        pos++;
                                                    }
                                        
                    filename = in+pos;
                    break;
                                }
                else
                {
                    redir_type = StdoutRedir;
                    in[pos++] = 0;
                    //IgnSpace(in, pos);
                    while(in[pos] == ' '){
                        pos++;
                    }
                    filename = in+pos;
                    //printf("debug: %s, %d\n", filename, redir_type);
                    break;
                }
            }
            else if(in[pos] == '<')
            {
                redir_type = StdinRedir;
                in[pos++] = 0;
                //IgnSpace(in, pos);
                while(in[pos] == ' '){
                    pos++;
                }
                                        
                filename = in+pos;
                //printf("debug: %s, %d\n", filename, redir_type);
                                            break;
            }
            else
            {
                pos--;
            }
        }
    }
                                        
    void Split(char* commandline){
                                        
        Check_redir(commandline);
                                        
        int i = 0;
        argv[i++] = strtok(commandline,SEP);
        while(argv[i++] = strtok(NULL,SEP));
        //解决ls无彩色问题
        if(strcmp(argv[0],"ls") == 0){
            argv[i-1] = (char*)"--color";
            argv[i] = NULL;
        }
    }
                                        
    int BuildingCmd(){
        int ret = 0;
        if(strcmp(argv[0],"cd") == 0){
            ret = 1;
            char* target = argv[1];
            //cd XXX 和cd
            if(!target) target = Home();//第二个参数为NULL
            //改变当前工作目录
            chdir(target);
            //处理target为..的情况
            //重新获取当前路径
            char temp[1024];
            getcwd(temp,1024);
            //更新当前环境变量PWD
            snprintf(pwd,SIZE,"PWD=%s",temp);
            //导出环境变量
            putenv(pwd);
        }
        else if(strcmp(argv[0],"export") == 0){
            ret = 1;
            if(argv[1]){
                strcpy(envtemp,argv[1]);
                putenv(envtemp);
            }
        }
        else if(strcmp(argv[0],"echo") == 0){
            ret = 1;
            if(argv[1] == NULL){
                printf("\n");
            }else{
                if(argv[1][0] == '$'){
                    if(argv[1][1] == '?'){
                        printf("%d\n",lastcode);
                        lastcode = 0;
                    }else{
                        char* e = getenv(argv[1]+1);
                        if(e) printf("%s\n",e);
                    }
                }
                else{
                    printf("%s\n",argv[1]);
                }
            }
        }
        return ret;
    }
                                        
    void Execute(){
        //只能交给子进程,如果用父进程执行命令行,执行一次就结束了
        pid_t id = fork();
        if(id < 0) perror("fork\n");
        else if(id == 0){
        int fd = -1;
        if(redir_type == StdinRedir)
        {
            fd = open(filename, O_RDONLY);
            dup2(fd, 0);
        }
        else if(redir_type == StdoutRedir)
        {
            umask(0);
            fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC,0666);
            dup2(fd, 1);
        }
        else if(redir_type == AppendRedir)
        {
            fd = open(filename, O_CREAT | O_WRONLY | O_APPEND);
            dup2(fd, 1);
        }
        else
        {
            // do nothing
        }
                                        
            execvp(argv[0],argv);
            exit(1);//执行完退出
        }
        //父进程等待子进程
        int status = 0;
        pid_t rid = waitpid(id,&status,0);
        if(rid == id) lastcode = WEXITSTATUS(status);//等待成功
    }
                                        
    int main(){
                                        
        while(1){
            char commandline[SIZE];
            //1. 打印命令行提示符,获取用户的命令字符串
            int n = Interactive(commandline,SIZE);
            if(!n) continue;//返回值为0就是空串,下面代码不再执行
            //2. 对命令行字符串进行切割
            Split(commandline);
            //3. 处理内建命令
            n = BuildingCmd();
            if(n) continue;
            //4. 执行这个命令
            Execute();
        }
        //int i;
        //for(i=0;argv[i];++i){
        //    printf("argv[%d]:%s\n",i,argv[i]);
        //}
        return 0;
    }
    

4、FILE

  • 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。

  • 研究以下代码看输出结果

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

int main(){    

   //int fd  = open("myfile",O_RDWR|O_CREAT|O_TRUNC,0666);    

   const char* msg1 = "hello printf\n"; // \n自带刷新    
   const char* msg2 = "hello fwrite\n";    
   const char* msg3 = "hello write\n";    

   write(1,msg3,strlen(msg3));    
   printf("%s",msg1);    
   fwrite(msg2,strlen(msg2),1,stdout);    

   fork();    
   return 0;    
}

结果如下:

但如果对进程实现输出重定向呢? ./buffer > buffile , 我们发现结果变成了:

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲

  • printf和``fwrite `库函数会自带缓冲区(之前的进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后。但是进程退出之后,会统一刷新,写入文件当中。但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新(刷新完相当于清空数据,就会发生写时拷贝)的时候,子进程也就有了同样的一份数据,随即产生两份数据。

  • write 没有变化,说明没有所谓的缓冲。

  • 图示:

  • 那么为什么会有缓冲区这个概念呢?
  • 可以提高使用者的效率
  • **减少系统调用次数:**调用系统调用是有时间和空间上的消耗的,增加缓冲区可以在数据填满过后再调用系统调用,减少调用系统调用的次数
  • **减少磁盘访问次数:**直接将数据写入磁盘会导致频繁的磁盘访问,而磁盘访问是一种相对较慢的操作。通过使用缓冲区,可以将多个写入操作合并到一起,减少磁盘访问次数,提高写入性能。
  • 模拟实现FILE结构体
  • mystdio.h文件

    #include<stdio.h>                                                                                                                                                     
                
    #define NONE_FLUSH (1<<1)
    #define LINE_FLUSH (1<<2)
    #define FULL_FLUSH (1<<3)
    #define SIZE 1024
                
    typedef struct _myFILE{
        int fileno;
        int pos;// 当前写入位置
        int cap;// 文件容量
        int flush_mode; // 刷新模式
        char buff[SIZE];
    }myFILE;
                
    myFILE* my_fopen(const char *path, const char *mode);
                
    int my_fwrite(const char *ptr, int size,myFILE *stream);
                
    //int my_fread(void *ptr, int size, myFILE *stream);
                
    void my_fclose(myFILE* stream);
                
    const char *toString(int flag);
                
    void DebugPrint(myFILE *fp);
    
  • mystdio.c文件

    #include "mystdio.h"                                                                                                                                                                                                                                                                                                                                                                   
    #include <fcntl.h>           
    #include <string.h>          
    #include <sys/types.h>       
    #include <sys/stat.h>        
    #include <stdlib.h>      
    #include <unistd.h>      
                      
    myFILE* my_fopen(const char *path, const char *mode){      
        int flag = 0;      
        if(strcmp(mode,"r") == 0){      
            flag |= O_RDONLY;      
        }else if(strcmp(mode,"w") == 0){      
            flag |= (O_WRONLY|O_CREAT|O_TRUNC);      
        }else if(strcmp(mode,"a") == 0){      
            flag |= (O_WRONLY|O_CREAT|O_APPEND);      
        }else{      
            return NULL;      
        }      
                      
        int fd = 0;      
        if(flag & O_WRONLY){      
            umask(0);      
            fd = open(path,flag,0666);      
        }else{      
            fd = open(path,flag);      
        }      
                      
        if(fd < 0) return NULL;      
                      
        myFILE* fp = (myFILE*)malloc(sizeof(myFILE));      
        if(!fp){      
            perror("malloc");      
            return NULL;      
        }      
                      
        fp->fileno = fd;      
        fp->pos = 0;      
        fp->cap = SIZE;      
        fp->flush_mode = LINE_FLUSH;      
                      
        return fp;      
    }      
                      
    void my_fflush(myFILE* stream){      
        if(stream->pos == 0) return;      
        write(stream->fileno,stream->buff,stream->pos);      
        stream->pos = 0;// 刷新后pos到最初位置      
    }      
                      
    int my_fwrite(const char *ptr, int size,myFILE *stream){      
        memcpy(stream->buff+stream->pos,ptr,size);// buff从pos开始       
        stream->pos += size;      
        if((stream->flush_mode & LINE_FLUSH) && (stream->buff[stream->pos-1] == '\n')){      
            my_fflush(stream);      
        }else if((stream->flush_mode & FULL_FLUSH) && (stream->pos == stream->cap)){      
            my_fflush(stream);      
        }      
                      
        return size;      
    }      
                      
    void my_fclose(myFILE* stream){      
        my_fflush(stream);      
        close(stream->fileno);      
        free(stream);      
    }      
                      
    const char *toString(int flag)      
    {      
        if(flag & NONE_FLUSH) return "None";      
        else if(flag & LINE_FLUSH) return "Line";      
        else if(flag & FULL_FLUSH) return "FULL";      
        return "Unknow";      
    }      
                      
    void DebugPrint(myFILE *fp)      
    {      
        printf("outbufer: %s\n", fp->buff);      
        printf("fd: %d\n", fp->fileno);      
        printf("pos: %d\n", fp->pos);      
        printf("cap: %d\n", fp->cap);      
        printf("flush_mode: %s\n", toString(fp->flush_mode));      
    } 
    
  • main.c文件

    #include "mystdio.h"    
    #include <string.h>    
    #include <unistd.h>    
                    
    int main(){    
        myFILE* fp = my_fopen("myfile","w");    
                    
        if(!fp) return 1;    
                    
        const char* msg = "hello my_fwrite\n";    
                    
        my_fwrite(msg,strlen(msg),fp);                                                                                                                                    
                    
        //int cnt = 5;    
        //char buffer[64];    
        //while(cnt)    
        //{    
        //    snprintf(buffer, sizeof(buffer), "helloworld,hellobit,%d  ", cnt--);    
        //    my_fwrite( buffer, strlen(buffer),fp);    
        //    DebugPrint(fp);    
        //    sleep(2);    
        //    //my_fflush(fp);    
        //}    
        //    
        //fork();    
                    
        my_fclose(fp);    
        return 0;    
    }
    

5、文件系统

我们在命令行输入ls -l ,除了看见文件名,还看见了一些文件的其他属性

其中还包含:

  • 模式
  • 硬链接数
  • 文件所有者
  • 文件所属组
  • 大小
  • 最后修改时间
  • 文件名

还可以通过stat命令来获取文件的更多信息

上面的执行结果有几个信息我们可以通过inode来解释一下


5.1、inode

为了能解释清楚inode,我们需要来理解一下文件系统,我们先看以下这张磁盘文件系统图:

磁盘是一个典型的块设备,磁盘分区被划分为一个个的block(块)。一个block的大小是在格式化的时候确定的,后面不可更改。

解释一下上述图片各名词的含义:

  • 块组(Block Group):文件系统会根据分区的大小划分为数个块组,一个块组就是一个分区,并且每个块组都会有相同的结构组成。

  • 超级块(Super Block):存放文件系统本身的信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了,不过一般磁盘不止一个扇区存放有超级块,可能会有三四个扇区存放有超级块,以便于第一个扇区的超级块损坏可以通过其他扇区的超级块来恢复。

  • 块组描述(Group Discriptor Table):描述块组属性信息,也就是管理磁盘块位图、inode为徒、inode表、数据块的使用情况。

  • 块位图(Block Bitmap):块位图中记录着数据块中哪个数据块已经被占用,哪个数据块没有被占用。

  • inode位图(inode Bitmap):每个bite表示该inode编号是否已经被使用,比如0表示没有被使用,1表示已经被使用。

  • inode表(inode Table):存放文件属性。如文件大小,所有者,最近修改时间,使用的块情况等

  • 数据块(Data Block):存放文件内容。

将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作的。

查看inode编号ls -li

从整体上来看,创建一个文件所经历的步骤如下:

为了说明问题,我们将上图简化:

创建文件一般需要4个操作:

  1. 存储属性

    内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。

  2. 存储数据

    该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。

  3. 记录分配情况

    文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

  4. 添加文件名到目录

    新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)(映射关系)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。


插写一个文件系统操作:

使用dd命令创建大文件并将其写入文件系统,然后将文件系统挂载到系统中,可以按照以下步骤进行:

  1. 创建目录 /path/to

    mkdir -p /path/to
    
  2. 使用 dd 命令创建大文件

    以下命令创建一个大小为1GB的空文件:

    dd if=/dev/zero of=/path/to/largefile bs=1M count=1024
    

    这将在指定路径创建名为 largefile 的大小为1GB的空文件。

  3. 使用 mkfs 创建文件系统

    在上面创建的文件中,可以使用适当的文件系统类型(例如ext4)来创建文件系统。假设要创建ext4文件系统:

    mkfs.ext4 /path/to/largefile
    

    这将在 /path/to/largefile 中创建一个ext4文件系统。

  4. 挂载文件系统

    创建文件系统后,您可以将其挂载到系统中的某个目录。首先,创建一个用于挂载的目录:

    mkdir /mnt/largefile
    

    然后将文件系统挂载到此目录:

    mount -o loop /path/to/largefile /mnt/largefile
    

    这将将 /path/to/largefile 中的文件系统挂载到 /mnt/largefile 目录中。


5.2、硬链接(Hard Link)

  • 硬链接是指在文件系统中,多个文件可以指向同一个inode,这样多个文件实际上指向了相同的数据块
  • 硬链接是文件系统级别的链接,它们只是指向了相同的inode,不保留路径信息,因此即使原文件移动或重命名,硬链接仍然有效。
  • 删除原文件并不会影响硬链接的可用性,只有当所有链接都被删除后,文件的inode和数据块才会被释放

使用硬链接:ln 原文件名 硬链接文件名,如下

ln file.txt file.hard

我们前面有讲到,文件的一行属性信息,有一个是硬链接数,我们能看到,一开始没给file.txt建立硬链接的时候,它的硬链接数1,就是只有自己,当给它建立硬链接后,硬链接的文件和原文件的硬链接数都变为2。

因此我们得出结论:普通文件的硬链接数等于该文件的建立的硬链接文件个数+1(可以自行再给file.txt文件再建立一个硬链接文件,看是不是硬链接数都变为3)。

我们再看目录的硬链接数:

我们看到,新创建的目录就有2个硬链接数!为什么是两个?我们看图可以知道,一个是自己的目录文件的,还有一个是该目录下的.目录的,我们知道目录.就是指当前目录。我们再来测试,在该目录下再创建5个目录,再看看硬链接数是多少?

能看到硬链接数变为7了!为什么呢?我们发现其中2个是自己和.目录,还有5个是该目录下的目录(a、b、c、d、e)下的..目录,显而易见..目录就是上级目录。因此我们得出结论:目录文件的硬链接数等于2+该目录下的目录个数。

注意:用户不能自己给目录建立硬链接,只能由系统创建!因为自己创建目录的硬链接会导致路径回路。

假如当前目录是/user/home,你给user目录创建一个硬链接/,哪当前创建的硬链接目录变为了/user/home/user,这就导致了路径回路。


5.3、软链接(Symbolic Link)

软链接也叫符号链接。

  • 软链接是一个特殊的文件,它包含了指向另一个文件的路径,相当于一个快捷方式
  • 软链接可以跨越文件系统,因为它们保存的是路径信息而不是inode号。
  • 软链接不像硬链接那样指向相同的数据块,而是指向目标文件的路径,因此即使原文件移动或重命名,软链接也可能失效。
  • 删除原文件后,软链接将成为死链接(即指向一个不存在的目标),如果不再需要,可以手动删除

使用软链接:ln -s 原文件 软链接文件。如:

ln -s file.txt file.symb


6、动态库和静态库

下面讲解动静态库可能会用到的文件

  • mystdio.h文件

    #include<stdio.h>
        
    #define NONE_FLUSH (1<<1)
    #define LINE_FLUSH (1<<2)
    #define FULL_FLUSH (1<<3)
    #define SIZE 1024
        
    typedef struct _myFILE{
        int fileno;
        int pos;// 当前写入位置
        int cap;// 文件容量
        int flush_mode; // 刷新模式
        char buff[SIZE];
    }myFILE;
        
    myFILE* my_fopen(const char *path, const char *mode);
        
    int my_fwrite(const char *ptr, int size,myFILE *stream);
        
    //int my_fread(void *ptr, int size, myFILE *stream);
        
    void my_fclose(myFILE* stream);
        
    const char *toString(int flag);
        
    void DebugPrint(myFILE *fp);
    
  • mystdio.c文件

    #include "mystdio.h"
    #include <fcntl.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <stdlib.h>
    #include <unistd.h>
        
    myFILE* my_fopen(const char *path, const char *mode){
        int flag = 0;
        if(strcmp(mode,"r") == 0){
            flag |= O_RDONLY;
        }else if(strcmp(mode,"w") == 0){
            flag |= (O_WRONLY|O_CREAT|O_TRUNC);
        }else if(strcmp(mode,"a") == 0){
            flag |= (O_WRONLY|O_CREAT|O_APPEND);
        }else{
            return NULL;
        }
        
        int fd = 0;
        if(flag & O_WRONLY){
            umask(0);
            fd = open(path,flag,0666);
        }else{
            fd = open(path,flag);
        }
        
        if(fd < 0) return NULL;
        
        myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
        if(!fp){
            perror("malloc");
            return NULL;
        }
        
        fp->fileno = fd;
        fp->pos = 0;
        fp->cap = SIZE;
        fp->flush_mode = LINE_FLUSH;
        
        return fp;
    }
        
    void my_fflush(myFILE* stream){
        if(stream->pos == 0) return;
        write(stream->fileno,stream->buff,stream->pos);
        stream->pos = 0;// 刷新后pos到最初位置
    }
        
    int my_fwrite(const char *ptr, int size,myFILE *stream){
        memcpy(stream->buff+stream->pos,ptr,size);// buff从pos开始 
        stream->pos += size;
        if((stream->flush_mode & LINE_FLUSH) && (stream->buff[stream->pos-1] == '\n')){
            my_fflush(stream);
        }else if((stream->flush_mode & FULL_FLUSH) && (stream->pos == stream->cap)){
            my_fflush(stream);
        }
        
        return size;
    }
        
    void my_fclose(myFILE* stream){
        my_fflush(stream);
        close(stream->fileno);
        free(stream);
    }
        
    const char *toString(int flag)
    {
        if(flag & NONE_FLUSH) return "None";
        else if(flag & LINE_FLUSH) return "Line";
        else if(flag & FULL_FLUSH) return "FULL";
        return "Unknow";
    }
        
    void DebugPrint(myFILE *fp)
    {
        printf("outbufer: %s\n", fp->buff);
        printf("fd: %d\n", fp->fileno);
        printf("pos: %d\n", fp->pos);
        printf("cap: %d\n", fp->cap);
        printf("flush_mode: %s\n", toString(fp->flush_mode));
    }
    
  • cal.h文件

    #pragma once                                                                       
            
    int Add(int a, int b);
    
  • cal.c文件

    #include "cal.h"    
            
    int Add(int a ,int b){    
        return a + b;                                                                  
    }
    
  • main.c文件

    #include <stdio.h>    
    #include "cal.h"    
    #include "mystdio.h"    
            
    int main(){    
        int res = Add(10,20);    
        printf("%d+%d=%d\n",10,20,res);    
            
        myFILE* fp = my_fopen("log1.txt","w");    
        if(fp) return 1;                                                               
        return 0;    
    }
    

6.1、静态库

静态库(.a文件):程序编译的时候把哭的代码链接到可执行文件中,程序运行时不再需要静态库

  • 生成静态库:将.o文件打包生成静态库

  • 使用归档工具:ar -rc 静态库名 .o文件

  • ar是gnu归档工具,rc表示(replace and create)

  • 这里的.o文件是用命令gcc -o 生成的(和动态库不一样,下面动态库会讲)

  • 需要注意的是:静态库名是有规范的,不能写成XXX.a,必须写成libXXX.a

  • 如:ar -rc libmylib.a *.o

  • 查看静态库列表ar -tv 静态库名

  • -t:列出静态库中的文件

  • -v:verbose 详细信息

  • 如:ar -tv libmylib.a

  • 使用静态库

  • 先将main.c生成main.o文件

    • 这里我们使用一个新命令 gcc -c .c文件,可以生成同名.o文件。如gcc -c main.c
  • 再使用命令:gcc - o 可执行文件 带main函数的目标文件(如main.o) -L 静态库路径 -l静态库名(去除前缀lib和后缀.a,如mylib) -static

  • gcc -o myexe main.c -L. -lmylib -static

  • 解释一下选项:

    • -L:指定静态库路径(当前路径可以使用.,也可以使用绝对路径)
    • -l:指定静态库
    • -static:指定是静态链接


6.2、动态库

动态库相对静态库的使用更复杂,原理也更复杂。

动态库(.so文件):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码(不像静态库那样直接在代码里面展开)。

在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)

动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

  • 生成动态库:动态库的生成也需要目标文件,但是动态库的目标文件和静态库的目标文件的生成方式不一样,动态库目标文件必须使用选项fPIC(产生位置无关码)。

    • 先将.c文件生成.o文件:gcc -c -fPIC *.c,将.c文件生成同名.o文件

    • 再使用选项shared.o文件打包成动态库:gcc -shared -o 动态库名 .o文件

    • gcc -shared -o libmylib.so *.o

    • 需要注意的是:这个动态库名也有规则:不能是XXX.so,必须是libXXX.so

  • 使用动态库

    • 先用main.c文件生成main.o文件

    • 再使用命令gcc -o 可执行文件 带main函数的目标文件 -L动态库路径 -l(动态库名去掉前缀lib,去掉后缀.so)

    • gcc -o myexe main.o -L. -lmylib

  • 现在有一个问题:如果动态库不在当前目录下呢?

    我们来测试一下,将动态库和头文件放到一个目录下

    假设我们得到了一个压缩文件,文件解压后得到一个目录文件,目录文件里面包含所需所有的头文件和静态库

    这时候我们怎么运行这个main函数呢?

    这里我们又会使用到一个选项I,这个选项是用来指明头文件的路径

    现在出现一个新的问题!直接运行myexe会报错!

    报错的原因是找不到动态库libmyib.so。我们来看看该可执行文件所依赖的动态库,使用命令:ldd 可执行文件名。ldd即list dynamic dependence。

    ldd myexe

    也就是没找到该动态库。有下面四种解决办法:

    1、将该文件所需的动态库放到系统里的动态库中(不建议使用,风险高)。

    如:sudo cp mylib/lib/* /lib64

    2、使用环境变量

    使用命令:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:动态库文件路径。这样导入环境变量的话,重启shell又需要重新配置。

    如:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xp2/Linux_Primary/day_32/demo_02/dynamiclib/user/mylib/lib

    还可以去系统文件添加环境变量

    在这两个文件中:.bash_profile.bashrc文件。查找到这两个文件的方式就是先cd到用户路径cd ~,再ls -al查看这个路径下所有文件就可以看到这两个文件。

    3、使用软链接的方式(推荐,简单好用)

    在lib64目录下新建一个同动态库名的动态库文件的软链接。

    如:sudo ln -s /home/xp2/Linux_Primary/day_32/demo_02/dynamiclib/user/mylib/lib/libmylib.so /lib64/libmylib.so

    4、在系统目录下(/etc/ld.so.conf.d/)新建一个(文件名.conf)配置文件(推荐)


那么好,Linux基础IO就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页

  • 77
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 38
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xpccccc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值