LINUX 文件和目录函数

与Linux 系统库函数 提供的open有些许不同,这次来讲解关于stdio.h C库种对文件的处理,但是本质上其实还是对open是进行的封装的。

从本院角度来看,两者的区别显而易见

  • open是UNIX系统调用函数(包括LINUX等),返回的是文件描述符(File Descriptor),它是文件在文件描述符表里的索引。
  • fopen是ANSIC标准中的C语言库函数,在不同的系统中应该调用不同的内核api。返回的是一个指向文件结构的指针。

如果从文件IO的角度来看:

  • 前者属于低级IO函数,
  • 后者属于高级IO函数。

低级和高级的简单区分标准是:谁离系统内核更近。低级文件IO运行在内核态,高级文件IO运行在用户态。

再跨平台这一块,当然还是fopen 比较OK

这一系列函数一样也是

对文件进行操作

打开,关闭,创建,删除,拓展,压缩等。

fopen

1
2
3
4
5
6
7
#include <stdio.h>

FILE *fopen(const char *pathname, const char *mode);

FILE *fdopen(int fd, const char *mode);//文件描述符

FILE *freopen(const char *pathname, const char *mode, FILE *stream);//文件输出重定向

提供了这么几个函数,我们来看看参数

  • const char * pathname
    • 文件的绝对路径
  • char *mode
    • r 只读,文本文件。
    • r+ 读写打开
    • w truncate file截断这个文件 并且将其初始0长度,再只写打开, 如果文件不存在也会创建。(文件原有数据会消失搭配r 就不会截断
    • w+ 文件不存在创建,存在就截断为0重写搭配r 就不会截断
    • a 追加当文件不存在创建,文件流的指针指向文件末尾
    • a+ 初始化文件流读的指针定位到
    • b 二进制打开
    • t 文本打开(默认都有)
  • return FILE*
    • 返回结构体指针,当失败是返回nullptr

fdopen 就是将fd,文件描述符转会为fd的简单操作。

  • 坑:不知道为什么open 打开的文件fd 再用fdopen 打开后无法写入和读取即使都rw权限拉满>

  • 问题: fread 参数没整对,

  • ```c
    NAME

       fread, fwrite - binary stream input/output
    

    SYNOPSIS

       #include <stdio.h>
    
       size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    
       size_t fwrite(const void *ptr, size_t size, size_t nmemb,
                     FILE *stream);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    - size_t size 参数 : 读取的 基本单元 字节大小 , 单位是字节 , 一般是 buffer 缓冲的单位大小 ;

    如果 buffer 缓冲区是 char 数组 , 则该参数的值是 sizeof(char) ;
    如果 buffer 缓冲区是 int 数组 , 则该参数的值是 sizeof(int) ;

    //错误就在于我直接sizeof(整个数组大小)了;

    fileno 函数就是反之将FILE* 转换为fd。

    freopen 后方多的这个参数呢其实就是文件重定向前的位置。一般常用三个值

    - stdout
    - stdin
    - stderr

    ```c

    FILE* test3 = freopen("1.txt", "w+", stdout);
    if (test3 == nullptr) {
    perror("文件3打开失败捏!");
    exit(0);
    }
    printf("%s(%d):%s\n", __FILE__, __LINE__, __FUNCTION__);

//可以打开文件发现stdout的内容被重定向到文件中了。

fread and fwrite

1
2
3
4
5
6
7
8
9
10
11
NAME
fread, fwrite - binary stream input/output

SYNOPSIS
#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);

  • void* ptr
    • fread读取的buf缓冲区,你可以new 堆上或者在栈上。大小最大么,根据系统,位数,来定。在堆上32位 4G 能单次分配2G 左右,64位呢能8G左右,相对于Windows, linux管理方式下 能使用的更多一点。
  • size_t size
    • 每次读取的字节数量: 怎么读的,按照多少字节分单位。每一次多少个大小
    • fread wirte 每次写的字节数量
  • size_t nmemb
    • 要读多少次。
    • 要写多少次。
  • FILE * stream
    • 要被操作的文件结构体指针。
  • return size_t
    • 需要注意的是:
    • 根据所设定的单位读了多少次,从0开始t 。
    • On success, fread() and fwrite() return the number of items read or
      written.  This number equals the number of bytes transferred only  when
      size  is 1.  If an error occurs, or the end of the file is reached, the
      return value is a short item count (or zero).
      

​ 文件流操作。

tip:

硬盘的读取,柱头,盘片,扇区。(这是机械硬盘才有的概念)所以就为了方便读取,则分盘符供给硬盘盒软盘使用。所以A,B,是分给软盘使用。而硬盘则分配了C盘

文件流的操作实际上就是与硬盘的读写取相对应,多少nmemb其实对应硬盘多少转,多少个扇区,然后size 对应磁头的偏移位置,每读写一次偏移多少。

fgetc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NAME
fgetc, fgets, getc, getchar, ungetc - input of characters and strings

SYNOPSIS
#include <stdio.h>

int fgetc(FILE *stream);

char *fgets(char *s, int size, FILE *stream);

int getc(FILE *stream);

int getchar(void);

int ungetc(int c, FILE *stream);

这些函数,所调用的文件结构体 必须以t模式打开,因为,判断条件为EOF 或者n , \0,当文件中没有EOF ,可能会出现意料之外的事。

需要注意的事中间需要在对一个文件进行读写时,需要注意文件内置的指针位置。获取位置可以用ftell 我们可以用fseek进行调整。rewind初始化到头的位置等。

fputc

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int fputc(int c, FILE *stream);

int fputs(const char *s, FILE *stream);

int putc(int c, FILE *stream);

int putchar(int c);

int puts(const char *s);

将知道哪个字符或者字符串写入文件流中,当然了,是从文件指针的位置开始传。

需要注意的是,由于这是C库函数,当然对C 类的字符串支持OK,其他类型的字符串的结尾 必须得是\0或者空

编译的时候编译器会将该函数报为不安全的函数,可能会导致栈溢出,等问题。所以呢提供了fputs_s稍微安全一点的函数来代替。

flush

   Note  that fflush() flushes only the user-space buffers provided by the
   C library.  To ensure that the data is physically stored  on  disk  the
   kernel  buffers  must  be  flushed  too,  for  example, with sync(2) or
   fsync(2).

作用与fsync类似,同样是将缓存区的数据写入内存中,确保写到内存里,操作的时间消耗是不可控的。

文件指针

每个文件进行读写时,都会内置一个文件指针,通过文件指针的操作我们可以随心所欲的修改文件数据的读写位置。

fseek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NAME
fgetpos, fseek, fsetpos, ftell, rewind - reposition a stream

SYNOPSIS
#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);

long ftell(FILE *stream);

void rewind(FILE *stream);

int fgetpos(FILE *stream, fpos_t *pos);

int fsetpos(FILE *stream, const fpos_t *pos);


fseek 函数说实话,作用跟lseek没差别,参数都一样。

需要注意的是fgetpos 的作用是64位的文件指针,64X的系统下文件的大小通常单次读写大小实际上很大,原有函数对64位的支持不是很好,所以就可以使用fgetpos64,来跳转64x位下的大文件。

注:fseek64也有64位,所以通常最常用的还是fseek

feof

NAME
clearerr, feof, ferror, fileno - check and reset stream status

SYNOPSIS

   #include <stdio.h>
1
2
3
4
5
6
7
void clearerr(FILE *stream);

int feof(FILE *stream);

int ferror(FILE *stream);

int fileno(FILE *stream);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

1
fileno(): _POSIX_C_SOURCE

可见,该函数是用来检测并且返回设置文件流的状态

feof文件未到尾部则返回 0,文件读到最后一个字节,并不会触发,即使末尾已经是空,还要再读1次,才会认为读到了eof,并返回1

ferror 则用于检测文件是否出现读写错误状态,通常用来检测上一个操作的错误问题。然后搭配clearerr清除错误状态。

clearerr清除错误状态后,在进行指针偏移的读写操作,即使超过文件范围依旧不会报错,seek本身在跳转的时和文件实际的大小不匹配一样是不会报错

需要注意的是,文件必须需要写权限,写权限可以将文件截断后再宏建

文件夹

c库并没有提供文件夹的创建函数,所以这是unix内核的函数。

mkdir

SYNOPSIS

   #include <sys/stat.h> //文件的状态的结构体等
   #include <sys/types.h> //宏,和各种类型。
1
2
3
4
5
6
int mkdir(const char *pathname, mode_t mode);

#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>

int mkdirat(int dirfd, const char *pathname, mode_t mode);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

   mkdirat():
       Since glibc 2.10:
           _POSIX_C_SOURCE >= 200809L
       Before glibc 2.10:
           _ATFILE_SOURCE

相对路径,生成在当前进程的工作目录下。

rmdir

   #include <unistd.h>

   int rmdir(const char *pathname);

DESCRIPTION
rmdir() deletes a directory, which must be empty.

删除文件夹,该文件夹必须是空文件夹

所以这玩意其实比较鸡肋,我们常常使用remove,这是一个C 库函数

remove

1
2
3
#include <stdio.h>

int remove(const char *pathname);

remove() deletes a name from the filesystem. It calls unlink(2) for files, and rmdir(2) for directories.

删除文件夹,并调用unlink 将文件夹中的文件断开连链接(硬链接,等于删除文件。当然当有进程使用,打开改文件管道,他会直到该文件关闭,再执行删除。

chown

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>

int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);

#include <fcntl.h> /* Definition of AT_* constants */
#include <unistd.h>

int fchownat(int dirfd, const char *pathname,
uid_t owner, gid_t group, int flags);


unix的系统函数,可以修改文件或者目录的用户组。

chanage owner ,修改所属, 并且修改的所属用户组,用户也必须修改。

不过一般用户组和用户的代码是相等的

当然了,这还是要提权的,没有权限啥也不是。当然了,如果修改自己所属组所属用户的东西。那就不需要进行提权。

链接删除,删除文件硬链接。当文件硬连接数为1 删除该文件。

UNLINK(2) Linux Programmer’s Manual UNLINK(2)

NAME
unlink, unlinkat - delete a name and possibly the file it refers to

SYNOPSIS

   #include <unistd.h>
1
2
3
4
5
6
int unlink(const char *pathname);

#include <fcntl.h> /* Definition of AT_* constants */
#include <unistd.h>

int unlinkat(int dirfd, const char *pathname, int flags);

linux文件与软硬链接

在linux下,文件有着三个重要的管理结构,

文件名,inode 元数据块

我们知道文件都有文件名与数据,这在 Linux 上被分成两个部分:用户数据 (user data) 与元数据 (metadata)。用户数据,即文件数据块 (data block),数据块是记录文件真实内容的地方\;而元数据则是文件的附加属性\,如文件大小、创建时间、所有者等信息。在 Linux 中,元数据中的 inode 号(inode 是文件元数据的一部分但其并不包含文件名,inode 号即索引节点号)才是文件的唯一标识而非文件名\。文件名仅是为了方便人们的记忆和使用,系统或程序通过 inode 号寻找正确的文件数据块。如图.展示了程序通过文件名获取文件内容的过程。

img

Linux 链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link)。默认情况下,ln 命令产生硬链接。

硬连接

硬连接指通过索引节点来进行连接。在 Linux 的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在 Linux 中,多个文件名指向同一索引节点是存在的。比如:A 是 B 的硬链接(A 和 B 都是文件名),则 A 的目录项中的 inode 节点号与 B 的目录项中的 inode 节点号相同,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件,A 和 B 对文件系统来说是完全平等的。删除其中任何一个都不会影响另外一个的访问。

硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。

软连接

另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类似于 Windows 的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。比如:A 是 B 的软链接(A 和 B 都是文件名),A 的目录项中的 inode 节点号与 B 的目录项中的 inode 节点号不相同,A 和 B 指向的是两个不同的 inode,继而指向两块不同的数据块。但是 A 的数据块中存放的只是 B 的路径名(可以根据这个找到 B 的目录项)。A 和 B 之间是“主从”关系,如果 B 被删除了,A 仍然存在(因为两个是不同的文件),但指向的是一个无效的链接。

总结

硬链接,非同名的同一个文件引用

软连接,快捷方式。

opendir

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
DIR *fdopendir(int fd);

struct dirent *readdir(DIR *dirp);

打开文件夹。

通常只是打开后还需要搭配readdir来进行使用

将文件夹结构体指针转为dirent 结构体

1
2
3
4
5
6
7
8
9
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};

d_type下有这么几种文件类型。通过判断可以过滤输出所需的文件值。

DT_BLK      This is a block device.

          DT_CHR      This is a character device.

          DT_DIR      This is a directory.

          DT_FIFO     This is a named pipe (FIFO).

          DT_LNK      This is a symbolic link.

          DT_REG      This is a regular file.

          DT_SOCK     This is a UNIX domain socket.

          DT_UNKNOWN  The file type could not be determined.

All applications must properly handle a return of DT_UNKNOWN.

不是同一个文件系统下的东西可能会返回DT_UNKNOWN

简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void dirsShow(const char* dirpath) {
printf("-----%s: \n", dirpath);
DIR* proof = opendir(dirpath);
if (proof == nullptr) {
perror("DIR open error");
exit(-1);
}
dirent* pCur = nullptr;

// 循环遍历该文件指针
do {
pCur = readdir(proof);
if (pCur == nullptr) {
perror("read Dir ");
exit(-1);
}
if (strcmp(pCur->d_name, ".") == 0 || strcmp(pCur->d_name, "..") == 0) {
continue;
}
if (pCur->d_type & DT_DIR) {
printf("%s(%d):%s link = %s\n", __FILE__, __LINE__, __func__,
pCur->d_name);
char bufname[MAXNAMLEN]{""};
snprintf(bufname, MAXNAMLEN, "%s/%s", dirpath, pCur->d_name);
// 递归调用
dirsShow(bufname);
}
} while (pCur != nullptr);
closedir(proof);
printf("------------: \n");
}

实现一个简单的显示子目录的所有文件夹,要实现一个tree 只需要在这个基础上修改就OK了

注意:该代码中藏了个简单的小问题解决(程序逻辑。