Linux环境编程-文件管理

发布时间 2023-09-09 16:52:35作者: 冲他丫的

一、一切皆文件

Linux/UNIX操作系统把所有的服务、设备、协议都抽象成文件的形式,提供了一套统一而简单的文件IO的系统调用,简称系统的文件IO

也就是说在UNIX\Linux中任何对象都可以被当做是某种特殊的文件,都可以像访问文件一样,访问这些对象

通过ls -l命令可以查看文件属性信息,其中行首第一个字符即代表该文件的文件类型。

以普通文件为例,使用 ls -l 命令,可以看到结果的第一列是 -rwxrwxrwx 的形式,其中第一个字符 "-" 表示这个文件为普通文件,它也可以是其他的字符,不同的字符代表不同类型的文件。其后的一串字符表明了该文件的权限,其中:

1)r 表明该文件具有可读权限,若该位置为 "-" ,则表明文件不可读;
        2)w 表明该文件具有写权限,若该位置为 "-" ,则表明文件不可写;
        3)x 表明该文件具有可执行权限,若该位置为 "-" ,则表明文件不具有可执行权限;
        4)第一个 rwx 表示该文件的所有者对该文件的权限;第二个 rwx 表示该文件所属组对该文件的权限;第三个 rwx 表示其他用户对该文件的权限。

文件分类:

普通文件 - 包括纯文本文件、二进制文件、各种压缩文件
目录文件 d 必须有读权限才能进入目录
块设备文件 b 保存大块数据的硬件设备,例如磁盘
字符设备文件 c 操作字符相关的设备 例如键盘、鼠标等
socket文件(套接字文件) s 通常用于网络通信
管道文件 p 用于进程间通信
链接文件 l 类似Windows的快捷方式
  • 普通文件

创建一个普通文件

touch newfile
  • 目录文件

Linux 中的目录也是文件,目录文件中保存着该目录下其他文件的 inode 号 和文件名等信息,目录文件中的每个数据项都是指向某个文件 inode 号的链接,删除文件名就等于删除与之对应的链接。目录文件的字体颜色是蓝色,使用 ls -l 命令查看,第一个字符为"d"(directory)。

创建一个目录文件:

mkdir directory
  • 块设备文件

块设备文件支持以块(block)为单位的访问方式。在 EXT4 文件系统中,一个 block 通常为 4KB 的大小,也就是说每次可以存取 4096(或其整数倍) 个字节的数据。应用程序可以随机访问块设备文件的数据,程序可以自行确定数据的位置,硬盘、软盘等都是块设备。使用 ls -l 命令查看,块设备文件的第一个字符是 "b"(block)。

  • 字符设备文件

字符设备文件以字节流的方式进行访问,由字符设备驱动程序来实现这种特性,这通常要用到 open、close、read、write 等系统调用。字符终端、串口和键盘等就是字符设备。另外,由于字符设备文件是以文件流的方式进行访问的,因此可以顺序读取,但通常不支持随机存取。使用 ls -l 命令查看,字符设备文件的第一个字符是 "c"(char)

  • 管道文件

创建一个管道文件:

mkfifo fifo_file

二、文件相关的系统调用

open

  • 打开文件
int open(const char *pathname, int flags);
    功能:打开文件
    pathname:文件的文件
    flags:打开文件的方式
        O_RDONLY    只读
        O_WRONLY    只写
        O_RDWR      读写
        O_APPEND    追加 
        O_CREAT     文件不存在则创建
        O_EXCL      配合O_CREAT,如果文件存在则失败
        O_TRUNC     文件如果存在,则清空打开
    返回值:文件描述符,类似于FILE*,代表了一个打开的文件(>=0),负数表示失败  
  • 创建文件
int open(const char *pathname, int flags, mode_t mode);
    功能:创建文件
    flags:O_CREAT
    mode:
        S_IRWXU  00700 拥有者 读写执行权限
        S_IRUSR  00400        读权限
        S_IWUSR  00200        写权限
        S_IXUSR  00100        执行权限
        S_IRWXG  00070 同组用户 读写执行权限
        S_IRGRP  00040         读
        S_IWGRP  00020          写
        S_IXGRP  00010          执行
        S_IRWXO  00007 其它用户 读写执行权限
        S_IROTH  00004          读
        S_IWOTH  00002          写
        S_IXOTH  00001          执行
        注意:可给宏名 也可以给 权限掩码 (0xxx) 八进制

实例:

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

int main(int argc,const char* argv[])
{
    int fd = open("hehe.txt",O_RDWR|O_CREAT,0644);
    if(0 > fd)
    {
        perror("open");
        return -1;
    }
    printf("ok\n");
}

creat

int creat(const char *pathname, mode_t mode);
    功能:创建文件
    mode:同上   

练习:测试fopen的各种打开方式与open的flags的对应情况

strace ./a.out

write

ssize_t write(int fd, const void *buf, size_t count);
    功能:把内存中的数据写入的文件中
    fd:文件描述符 open creat的返回值
    buf:待写入的内存的首地址
    count:要写入的字节数
    返回值:成功写入的字节数   

实例:

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

int main(int argc,const char* argv[])
{
    int fd = open("hehe.txt",O_WRONLY|O_TRUNC|O_CREAT,0644);
    if(0 > fd)
    {
        perror("open");
        return -1;
    }

    for(int i=0; i<10; i++)
    {
        write(fd,&i,sizeof(i));    
    }

    close(fd);
}

read

  ssize_t read(int fd, void *buf, size_t count);
    功能:从文件中读取数据到内存中
    fd:文件描述符 open creat的返回值
    buf:存储读取出来的数据的内存首地址
    count:要读取的字节数
    返回值:成功读取的字节数  

实例:

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

int main(int argc,const char* argv[])
{
    int fd = open("hehe.txt",O_RDONLY);
    if(0 > fd)
    {
        perror("open");
        return -1;
    }

    int num = -100;
    while(0 < read(fd,&num,sizeof(4)))
    {
        printf("%d\n",num);
    }

    close(fd);
}

练习:分别使用标准IO和系统IO写入一百万个整数到文件,测试谁的时间更短?为什么?

结论:在同等数据的写入下,使用标准IO要比直接使用系统IO更快

原因:标准IO有缓冲区机制,在执行fwrite写文件时,数据不是直接调用系统IO写入磁盘,而是先存放在内存的缓冲区中,直到缓冲区满后才会调用一次系统IO全部写入到磁盘,因此对系统IO的调用次数、用户态内核态的转换次数都大大降低

而直接使用系统IO是没有缓冲区机制,因此耗时增加

解决:如果给系统IO也加上缓冲区机制,那么它的速度要比标准IO更快

三、随机读写fseek/lseek

每个打开的文件都有一个记录读写位置的指针,也称文件位置指针,对文件的读写时该指针会自动往后移动,因此顺序读写时无需操作

当需要去文件的任意位置进行读写时,才需要调整该指针的位置

  • 标准IO:fseek
int fseek(FILE *stream, long offset, int whence);
        返回值:成功0 失败-1        

使用:

fseek(FILE *stream, 0, SEEK_SET);     //将读写位置移动到文件开头
fseek(FILE *stream, 0, SEEK_END);     //将读写位置移动到文件尾
  • 系统IO:lseek
off_t lseek(int fd, off_t offset, int whence);
        返回值:成功返回调整后位置指针的位置 失败-1       

使用:

lseek(int fd, 0, SEEK_SET);     //将读写位置移到文件开头
lseek(int fd, 0, SEEK_END);     //将读写位置移到文件尾时/获取文件长度
lseek(int fd, 0, SEEK_CUR);     //取得目前文件位置
lseek(fd,100,SEEK_END);     //扩展文件长度,增加了100字节,,但是扩展完需再写一次数据,否则扩展无效

系统IO中的文本文件的读写:

在系统IO中没有类似fprintf\fscanf函数,因此没有直接对文本文件读写的操作。但是可以通过把数据转换成字符串进行间接的文本文件读写

  • 写文本文件

任意类型数据对象 sprintf 转换成字符串 write写入

  • 读文本文件

按字符串格式读取到内存 sscanf 解析到任意类型数据中

实例:

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

typedef struct Student
{
    char name[20];
    int age;
    float grade;
}Student;

int main(int argc,const char* argv[])
{
    int fd = open("hehe.txt",O_RDWR|O_TRUNC);
    printf("fd %d\n",fd);
    if(0 > fd)
    {
        perror("open");
        return -1;
    }
    /*
    char str[] = "hehexixi";
    write(fd,str,sizeof(str)-1);
    */
    Student stu = {"张三",20,88.8};
    Student stu1 = {};
    char buf[4096] = {};
    sprintf(buf,"%s %d %g",stu.name,stu.age,stu.grade);
    write(fd,buf,strlen(buf));

    lseek(fd,0,SEEK_SET);
    char buf1[4096] = {};
    read(fd,buf1,sizeof(buf1));
    sscanf(buf1,"%s %d %g",stu1.name,&stu1.age,&stu1.grade);
    printf("sut1:%s %d %g\n",stu1.name,stu1.age+88,stu1.grade);
    close(fd);
}

四、文件描述符fd

1、非负整数,代表了一个打开的文件

2、通过系统调用(open\creat)返回,该数值是被内核使用的

3、它在内核中对应一个内核对象,因为内核不能暴露它真实的地址,因此不能直接返回真实的文件地址,而是使用文件描述符来表示

4、内核中有一张记录了所有已打开的文件的二维表,文件描述符就是访问该表每行的下标,文件描述符也称为句柄,就是访问文件的凭证

5、内核中有三个默认长期打开的文件描述符

0 标准输入 STDIN_FILENO stdin
1 标准输出 STDOUT_FILENO stdout
2 标准错误 STDERR_FILENO stderr

文件描述符的重定向

  • dup
int dup(int oldfd);
    功能:复制一个已经打开的文件描述符
    返回值:返回一个当前进程没有使用的最小的文件描述符,该描述符与oldfd对应一个文件
  • dup2
int dup2(int oldfd, int newfd);
    功能:复制oldfd文件描述符为newfd

如果newfd已经被打开,那么会先关闭newfd再复制

相当于newfd\oldfd同时对应原oldfd对应的文件

重定向的作用:

把一个已经打开的文件描述符fd 经过dup2指定复制为标准输入0\标准输出1\标准错误2,则会先关闭已经打开的0\1\2,然后0\1\2重定向指向fd对应的文件

就可以通过printf\scanf函数就可以直接往fd对应的文件进行读写,相当于write\read

但是在当前进程中会导致后续无法使用标准输入、输出、错误文件,因此可以在开始前通过dup先备份这些文件,结束后通过dup2重新指向标准文件

实例:

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

int main(int argc,const char* argv[])
{
    int oldfd = dup(1);

    int fd = open("hehe.txt",O_RDWR|O_TRUNC);
    if(0 > fd)
    {
        perror("open");
        return -1;
    }

    dup2(fd,1);
    printf("hehehxixixixix\n");
    fflush(stdout);
    dup2(oldfd,1);
    printf("xixi\n");
    close(fd);
    close(oldfd);
}

五、文件同步

1、在写入数据时,内存与磁盘之间有一块缓冲区,目的是为了降低磁盘的读写次数、提高读写效率

2、但是这种机制带来的后果是磁盘中的数据与实际写入的数据可能不符合,系统提供了三个函数让缓冲区中的数据立即写入磁盘,称为文件同步

  fsync\sync\fdataync      

六、文件属性stat

struct stat {
        dev_t     st_dev;         // 设备ID
        ino_t     st_ino;         // inode节点号
        mode_t    st_mode;        // 文件的类型和权限
        nlink_t   st_nlink;       // 硬链接数
        uid_t     st_uid;         // 用户ID
        gid_t     st_gid;         // 组ID
        dev_t     st_rdev;        // 特殊设备ID
        off_t     st_size;        // 总字节数
        blksize_t st_blksize;     // IO块总字节数
        blkcnt_t  st_blocks;      // 占用大小512字节的内存块数量
        struct timespec st_atim;  // 最后访问时间
        struct timespec st_mtim;  // 最后修改时间
        struct timespec st_ctim;  // 最后状态修改时间

        #define st_atime st_atim.tv_sec      /* Backward compatibility */
        #define st_mtime st_mtim.tv_sec
        #define st_ctime st_ctim.tv_sec
    };

相关函数:

  • stat
int stat(const char *pathname, struct stat *buf);
    功能:根据文件路径获取文件的属性
    buf:输出型参数   
  • fsat
int fstat(int fd, struct stat *buf);
    功能:根据文件描述符获取文件的属性
  • lstat
int lstat(const char *pathname, struct stat *buf);
    功能:根据文件路径获取软链接文件的属性   

实例:

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

//    显示文件类型
void file_type(mode_t mode)
{
    switch(mode & S_IFMT)
    {
        case S_IFSOCK:    printf("s"); break;    
        case S_IFLNK:    printf("l"); break;    
        case S_IFREG:    printf("-"); break;    
        case S_IFBLK:    printf("b"); break;    
        case S_IFDIR:    printf("d"); break;    
        case S_IFIFO:    printf("p"); break;    
    }
}

//    显示文件权限
void file_mode(mode_t mode)
{
    printf("%c",mode & S_IRUSR?'r':'-');    
    printf("%c",mode & S_IWUSR?'w':'-');    
    printf("%c",mode & S_IXUSR?'x':'-');    
    printf("%c",mode & S_IRGRP?'r':'-');    
    printf("%c",mode & S_IWGRP?'w':'-');    
    printf("%c",mode & S_IXGRP?'x':'-');    
    printf("%c",mode & S_IROTH?'r':'-');    
    printf("%c",mode & S_IWOTH?'w':'-');    
    printf("%c",mode & S_IXOTH?'x':'-');    
}

//    显示用户名
void user_name(uid_t uid)
{
    struct passwd* pwd = getpwuid(uid);
    printf("%s ",pwd->pw_name);
}

//    显示组名
void group_name(gid_t gid)
{
    struct group* grp = getgrgid(gid);
    printf("%s ",grp->gr_name);
}

//    显示最后时间
void show_time(time_t st_time)
{
    struct tm* t = localtime(&st_time);
    printf("%d月\t%d %02d:%02d ",t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min);
}

//    ls -l
void list_file_stat(const char* path)
{
    //    获取文件属性
    struct stat buf = {};
    if(stat(path,&buf))
    {
        perror("stat");
        return;
    }

    file_type(buf.st_mode);
    file_mode(buf.st_mode);
    printf(" %d ",S_ISDIR(buf.st_mode)?2:1);
    user_name(buf.st_uid);
    group_name(buf.st_gid);
    printf("%lu ",buf.st_size);
    show_time(buf.st_mtime);
    printf("%s\n",path);
}

int main(int argc,const char* argv[])
{
    list_file_stat("a.out");    
}

七、文件的权限access/chmod

linux文件的权限可以分为四类:可读、可写、可执行、没有权限。分别用字符r、w、x、- 表示。

在linux命令行中,输入:ls -la, 可以查看当前目录下面所有文件的权限。

  • 权限的字母表示和数字表示

777为什么对应rwxrwxrwx?

777每一位数字转化为二进制表示就是111 111 111,
9个字母(rwxrwxrwx)与9个数字(111111111)位置一一对应,
如果字母对应的数字为1,那么就说明该字母所表示的权限生效,
如果字母对应的数字为0,那么就说明该字母所表示的权限无效,相应字母位置用-替代。

根据以上规则所以我们可以推出666对应的字母表示
666   ===>    110 110 110   ===>   rw-rw-rw-

相关函数

  • access:测试当前用户对文件的权限
int access(const char *pathname, int mode);
    功能:测试当前用户对文件的权限
    pathname:想要测试的文件路径
    mode:想要测试的权限
        F_OK    测试文件是否存在
        R_OK    测试文件是否有读权限
        W_OK    测试文件是否有写权限
        X_OK    测试文件是否有执行权限
    返回值:存在返回0 不存在返回-1
  • chmod
int chmod(const char *pathname, mode_t mode);
    功能:根据文件路径修改文件权限
    mode:由三个八进制数组成的权限掩码
        0xxx
        0644    0666   普通文件
        0755           可执行文件    
  • fchomd
int fchmod(int fd, mode_t mode);
    功能:根据文件描述符修改文件权限

权限屏蔽码/权限掩码

umask 值是用于禁用文件或目录的默认权限

如果想让新创建(open\creat)的文件不具备某些权限,可以通过设置权限屏蔽码进行屏蔽权限

查看命令 umask 查看当前终端的权限屏蔽码

注意:可以通过chmod命令、函数可以无视权限屏蔽码

  • 计算用户的真实文件权限

问:
用户的默认文件权限为666,
假设用户的权限掩码为002,
那么用户的真实权限是?

答:
666  ===> 110 110 110 ===> rw-rw-rw-
002  ===> 000 000 010 ===> -------w-
(rw-rw-rw- )减去  (-------w-)  等于  (rw-rw-r--)
所以用户的真实文件权限为rw-rw-r--  ===>  110 110 100 ===> 664

查看用户的权限:umask

修改用户的权限掩码:umask xxx (修改成xxx)
注意:只是当前终端临时有效,如果想要长期有效需要修改配置文件
 mode_t umask(mode_t mask);
       mask:新的屏蔽码
       返回值:原来的屏蔽码
注意:该函数只是当前进程中生效,进程结束后就失效了     

八、修改大小、删除、重命名

1、修改文件的大小 truncate

  • truncate:根据文件路径截取文件长度
int truncate(const char *path, off_t length);
    功能:根据文件路径截取文件长度
    length:截取后文件的长度(字节)
  • ftruncate  :  根据文件描述符截取文件长度
int ftruncate(int fd, off_t length);
    功能:根据文件描述符截取文件长度

2、文件删除、重命名 remove/rename

  • remove
int remove(const char *pathname);
    功能:由C标准库提供的删除文件函数
  • unlink
int unlink(const char *pathname);
    功能:操作系统提供的删除文件函数  
  • remove底层调用unlink、rmdir函数

1、如果删除的是硬链接文件或者原文件本身且文件是关闭的,则是把该文件的硬链接数减1,然后清除该文件的inode信息

2、当文件的硬链接数减为0时,系统就把该文件的block权限释放,可以用于存储其它文件的数据

3、如果删除软链接文件,则删除的是软链接文件的inode信息,而不会删除原文件的数据,没有任何一个函数可以通过软链接文件删除源文件

  • rename
int rename(const char *oldpath, const char *newpath);
    功能:重命名文件

九、软、硬链接

Linux文件系统会把分区分为两大部分:

  • inode信息块区:

每块默认128B,记录某个文件的文件权限、大小、所有者、修改时间等属性信息以及该文件的block信息

  • block数据块区:

每块默认4Kb,记录了文件的真正内容数据、文件名

每个文件有且只有一个inode信息块以及若干个block数据块,读取文件file需要借助所在目录文件的block中记录的file文件的inode号和文件名,来找到file的inode信息块,从而读取file文件的block数据块

硬链接文件:

没有属于自己的inode和block,只是在不同的目录下复制链接的源文件的inode信息块,通过该复制的inode信息块访问源文件的block数据块

软链接文件:

软链接文件会创建自己的新的inode和block块,在软链接文件的block中不存储源文件的数据,而是存储源文件的inode号,从而借助源文件的inode来访问源文件的block

区别:

1、删除源文件,只是删除源文件的inode信息块中的内容,对应的block的数据不会清理,所以硬链接文件依然可以访问,但是软链接文件就无法访问了

2、当文件的硬链接数删除到0时,文件才算被真正的删除了

3、修改硬链接文件的内容,源文件也会随之修改

4、硬链接不能链接目录、软链接可以

5、硬链接不能跨文件系统使用,而软链接可以

相关函数

  • link:创建硬链接文件
int link(const char *oldpath, const char *newpath);
    功能:创建硬链接文件
  • symlink    :创建软链接文件 文件类型 l
int symlink(const char *target, const char *linkpath);
    功能:创建软链接文件 文件类型 l

十、目录操作:

  • mkdir
int mkdir(const char *pathname, mode_t mode);
    功能:创建空目录
    mode:目录的权限,必须有执行权限才能进入目录
    返回值:成功0 失败-1
  • rmdir
int rmdir(const char *pathname);
    功能:删除空目录
  • chdir
 int chdir(const char *path);
    功能:修改当前的工作路径为path 相当于cd  
  • getcwd
char *getcwd(char *buf, size_t size);
    功能:获取当前进程的工作路径 相当于pwd    
  • opendir
DIR *opendir(const char *name);
    功能:根据目录路径打开目录
    返回值:成功返回一个目录流对象  
  • fdopendir
DIR *fdopendir(int fd);
    功能:根据目录文件描述符打开目录
    返回值:成功返回一个目录流对象 
注意:DIR目录流对象中记录了该目录中所有文件的信息  
  • readdir
struct dirent *readdir(DIR *dirp);
    功能:从目录流dirp中读取一条文件信息并返回
struct dirent {
        ino_t  d_ino;       // inode号
        off_t  d_off;       // 距离下一条信息的字节数
        unsigned short d_reclen;  // 当前信息的字节数
        unsigned char  d_type;   // 文件类型
        char           d_name[256]; // 文件名
    };    

注意:readdir读取完一条信息后,会自动的指向下一条信息,只需要继续执行readdir即可读取下一条,直到返回值为NULL读取完毕

作业1:

实现一个函数,可以删除某文件的[n,m)个字节

int del_file_part(const char* path,size_t n,size_t m)

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

bool del_file_part(const char* path,size_t n,size_t m)
{
    int fd = open(path,O_RDWR);
    if(0 > fd)
    {
        perror("open");
        return false;
    }

    //    计算文件大小
    int len = lseek(fd,0,SEEK_END);
    if(n > len) return false;
    if(m > len) m = len;

    char buf[4096] = {};
    lseek(fd,m,SEEK_SET);
    int ret = 0;
    while(ret = read(fd,buf,sizeof(buf)))
    {
        lseek(fd,n,SEEK_SET);
        write(fd,buf,ret);
        m += ret;
        n += ret;
        lseek(fd,m,SEEK_SET);
    }

    ftruncate(fd,len-(m-n));
    close(fd);
    return true;
}

int main(int argc,const char* argv[])
{
    printf("%d\n",del_file_part("hehe.txt",5,10));
}