Bootstrap

03-系统调用

一、系统调用的概述

1.系统调用介绍

系统调用是操作系统提供给用户用来操作内核服务的一组接口(函数)的统称。

  • 为什么要通过系统调用来访问系统资源?

    • 因为系统资源不希望被用户随意访问,可能造成各种意想不到的错误,也不方便资源的统一管理,但是又不能不让用户访问,因此就限制了用户只能通过指定的入口和指定的方法去访问内核资源,而我们并不需要知道入口在哪,因为系统调用已经帮我们封装好了,我们只需要调用相应的接口就好了
  • 运行的程序分为用户态和内核态:运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的进程各种操作都受限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别等。

  • 操作系统一般是通过软件中断从用户态切换到内核态;但不管通过什么方法访问内核资源,都需要先通过系统调用,如下图:

在这里插入图片描述

2.库函数和系统调用对比

  1. 系统调用是操作系统提供给用户操作内核服务的函数;
  2. 库函数是c/c++官方提供的,或者第三方提供的函数,库函数分为两类:
    • 不需要调用系统调用的库函数:不需要切换到内核空间就可以完成函数功能的函数,如 strcpy;
    • 需要系统调用的函数:需要切换到内核空间才能完成相应的功能的函数,这类函数都封装了系统调用,如 printf。
  • 系统调用比较消耗时间,因为发生系统调用,cpu 需要在用户态和内核态来回切换,从用户态切换到内核态,系统要先保存用户态运行状态,然后切换到内核态,系统调用完再切换回用户态,这个过程消耗时间。因此,为了降低切换的次数,在用户态设置了缓冲区,会一次将多个数据存入缓冲区,然后再从缓冲区读取,就不需要一个个读写,频繁在两种状态切换了。
  • 库函数有缓冲区,系统调用没有缓冲区,因为系统调用是直接操作内核,不需要在两种状态切换,因此没有缓冲区。

二、文件描述符

1.什么是文件描述符

在 Linux 系统中,不仅仅是我们平时认为的文本文件、C语言文件才叫文件。在系统看来,一切设备皆文件。操作系统会用一个非负整数来标示进程已经打开的文件,这个非负整数就叫做文件描述符。

  • 操作系统会为每一个进程默认打开3个文件描述符:即 0 1 2

    1. 0:代表标准输入设备,如电脑的键盘,可以通过 scanf 默认获取键盘输入;
    2. 1:代表标准输出设备,如终端屏幕,可以通过 printf 默认将数据输出到终端;
    3. 2:代表标准错误输出设备,如C语言中用到的 perror 能打印错误信息。
  • 系统会为每个进程分配 4G(抽象的4G,并不是实实在在的4G) 的空间,其中 3G 的用户空间,1G 的内核空间,而文件描述符就存放在内核空间的 PCB 进程控制块中的文件描述表中。

2.文件描述表

文件描述表用来存放和管理文件描述符,系统默认每个文件表最多存放1024(0 ~ 1023)个文件描述符,文件描述表是通过位图来管理当前进程打开的所有文件描述符的,会默认打开 0 1 2 三个文件描述符,如图:

文件描述符状态11100… …0
文件描述符编号01234… …n

图上每一个为一个二进制位,0 代表文件描述符未使用,1 代表已使用,每次打开新文件,会选择最小可用的文件描述符,即在状态为 0 的里面选一个编号最小的来用。

  • 可用通过ulimit -a命令查看 Linux 文件描述符的限制:
edu@edu:~/study/my_code$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15587
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024 # 默认可打开的文件描述符个数
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15587
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

  • 每个进程最多打开的文件描述符个数只是默认为1024,但可以通过命令ulimit -n 描述符数修改每个进程默认可打开的文件描述符的个数。

三、文件操作

1.磁盘文件权限

磁盘文件权限是指直接对文件的读、写、执行操作,如 win 电脑里面打开一个文件,保存修改的时候会提示文件只读,这就是其磁盘没有写的权限,或者在 Linux 系统里 ./文件名 运行一个非可执行文件名的时候无法运行,因为磁盘文件没有可执行权限。

  • 可以通过命令ls -lh 文件名查看文件的权限信息:
edu@edu:~/study/my_code$ ls a.txt -lh
-rw-rw-r-- 1 edu edu 12 1229 09:25 a.txt
  • 说明:

    1. 可以看到最前面有10个连续的字符,第一个代表文件的类型;
    2. 后面九个可分为三组,代表不同身份的磁盘权限:
      • 前三个代表当前登录用户的磁盘文件权限;
      • 中间三个代表同组用户的磁盘文件权限;
      • 后三个代表其它用户的磁盘文件权限;
    3. 文件权限可分为:r可读、w可写、x可执行,如果是 - 代表不具备这个权限。
  • 文件的权限状态就两种,即有和无,因此可以通过二进制的 0 表示无这个权限,1 代表有这个权限:

r w x ~ r w x
0 0 0   1 1 1	0 - 7 # 三位二进制为一位八进制
则每种权限的八进制为:
可读:1 0 0 - 4
可写:0 1 0 - 2
执行:0 0 1 - 1
也可以是组合:
可读可写:1 1 0 - 6
可读执行:1 0 1 - 5
可写执行:0 1 1 - 3
读写执行:1 1 1 - 7

上面的都只能代表一个用户,代表三种用户权限的写法:
0777 - 所有用户可读可写可执行
0664 - 当前所有者和同组用户可读可写,其它用户只读
... ...
  • 说明:如果磁盘文件权限可读可写,文件操作的时候设置文件以读写文件打开才有效,如果磁盘文件权限都不可读不可写,那么文件操作的时候,也无法读写操作,可以通过 命令chmod 权限mode 文件名后期设置文件权限,但建议一般在创建的时候就设置好磁盘权限。

2.打开文件

2.1 open 函数介绍

  • 函数介绍
#include <sys/types.h>
#include <sys/stat.h> # 要包含的头文件,如果记不住头文件,可以通过man命令查看
#include <fcntl.h> 

函数一:
int open(const char *pathname, int flags);
函数二:
int open(const char *pathname, int flags, mode_t mode);
功能:打开文件,如果文件不存在则可以选择创建。
参数:
	pathname:文件的路径及文件名
	flags:打开文件的行为标志,必选项 O_RDONLY, O_WRONLY, O_RDWR
	mode:这个参数,只有在文件不存在时有效,指新建文件时指定文件的磁盘权限
返回值:
	成功:成功返回打开的文件描述符(当前进程最小可用的文件描述符)
	失败:-1
  • 说明:这里打开文件的函数有两个,第一个用于打开已经存在的文件,因为文件已经存在,其文件磁盘权限已经确定;第二个用于打开不存在的文件,创建文件的时候需要添加文件的磁盘权限。

2.2 flags 文件操作权限

flags 权限和磁盘权限不同,它是用于我们通过 open 打开文件以后,可以通过文件描述符对文件执行的读写等权限。

  • 其分为必选项和可选项:
  • 必选项:
    1. O_RDONLY ----- 以只读的方式打开;
    2. O_WRONLY ----- 以只写的方式打开;
    3. O_RDWR ----- 以可读、可写的方式打开。
  • 可选项:和必选项按位或起来
    1. O_CREAT ----- 文件不存在时创建文件,使用此选项时需使用 mode 说明文件的权限,即上面的 open 函数二打开;
    2. O_EXCL ----- 如果同时指定了 O_CREAT,且文件已经存在,报错;
    3. O_TRUNC ----- 如果文件存在,则清空文件内容;
    4. O_APPEND ----- 写文件时,数据添加到文件末尾;
    5. O_NONBLOCK ----- 对于设备文件, 以 O_NONBLOCK 方式打开可以做非阻塞 I/O。

2.3 mode 文件磁盘权限

磁盘权限三种身份,以及三种权限的各种组合上面已经演示,这里补充掩码 umask。

即使我们在创建文件的时候设置了 mode 磁盘权限,但文件的实际磁盘权限也不一定是 mode 权限,而是:mode & ~umask

  • 查看 Linux 的默认掩码:
edu@edu:~/study/my_code$ umask
0002
  • 说明:
    1. umask 只能屏蔽通过 open 创建的文件,无法屏蔽 chmod 修改的文件磁盘权限;
    2. 根据默认掩码可以看出,只有八进制的最低位有数字,其它全0,说明默认只屏蔽了这一位对应的其它用户的权限;
    3. 屏蔽原理,通过二进制来看,会先将要屏蔽的权限置1,其它置0,取反,则要屏蔽的变0,其它变1,按位与上 mode 权限,按位与的特点,遇0置0,遇1保持不变。因此就将 mode 权限指定位置0了,屏蔽了指定权限,如:
创建一个文件 mode 为 0777,默认 umask 为0002
mode & ~umask --> 0777 & ~0002
转化为2进制:
0002 = 000 000 010
~0002 = 111 111 101
按位与:
111 111 111 & 111 111 101 = 111 111 101 = 0775
因此可以知道默认屏蔽了其它用户的可写权限w
  • 可以通过umask mode命令设置权限,不过只对当前终端有效,通过umask -S查看各组用户的默认权限:
edu@edu:~/study/my_code$ umask -S
u=rwx,g=rwx,o=rx

2.4 open 打开文件实操

  • 关闭文件函数
#include <unistd.h>
int close(int fd);
功能:关闭已打开的文件
参数:
	fd: 文件描述符,open()的返回值
返回值:
	成功:0
	失败:-1, 并设置 errno
  • 这里演示函数二,打开一个不存在的文件:
// 创建函数,打开文件
void test01()
{
    // 打开文件
    int fd = open("c.txt", O_WRONLY | O_CREAT, 0777);
    // 判断文件是否打开成功
    if (fd < 0)
    {
        perror("open");
        return;
    }
    printf("文件打开成功\n");
    // 关闭文件
    close(fd);
}
  • 运行结果
edu@edu:~/study/my_code$ gcc 03-file_operator.c
edu@edu:~/study/my_code$ ./a.out
文件打开成功
edu@edu:~/study/my_code$ ls c.txt -lh
-rwxrwxr-x 1 edu edu 0 12月 30 09:38 c.txt
  • 说明:
    1. 文件操作要遵循基本步骤,先打开文件,然后判断是否打开成功,如果打开失败返回 -1,又没有进行判断,则会对一个不存在的文件描述符操作,用完一定记得关闭文件,否则进程结束前会一直占用该文件描述符;
    2. 可以看到我们创建文件设置的 mode 为 0777,但实际文件中其它用户的可写权限被屏蔽了。

3.文件的读写操作

3.1 write 文件的写操作

  • 函数介绍
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
	fd:表示往哪个文件描述符写入数据
	buf:需要写入的内存数据的起始地址,void *表示可以写入任意类型的数据
	count:需要写入的字节数
返回值:
	成功:实际写入的字节数
	失败:-1
  • 代码演示:
void test02()
{
    // 打开文件
    int fd = open("c.txt", O_WRONLY);

    if (fd < 0)
    {
        perror("open");
        return;
    }
    printf("文件打开成功\n");

    // 写入数据
    write(fd, "hello world!\n", strlen("hello world!\n"));

    // 关闭文件
    close(fd);
}
  • 运行结果
edu@edu:~/study/my_code$ cat c.txt
edu@edu:~/study/my_code$ gcc 03-file_operator.c
edu@edu:~/study/my_code$ ./a.out
文件打开成功
edu@edu:~/study/my_code$ cat c.txt
hello world!
  • write 是系统调用,直接对内核操作,不需要缓冲区,这里可以验证:
void test03()
{
    // 直接往终端写数据
    write(1, "hello world!", strlen("hello world!"));
    while (1);
}
  • 运行结果
hello world!# 一直阻塞在这里
  • 说明:
    1. 上面是直接将数据写入描述符 1 ,即标准输出设备,我们的虚拟机终端;
    2. 通过死循环阻塞是为了保证没有结束刷新,同时也没有行刷新,满刷新和强制刷新,内容依旧可以在终端输出,证明了 write 系统调用的函数没有缓冲区。

3.2 read 文件的读操作

  • 函数介绍
#include <unistd.h
ssize_t read(int fd, void *buf, size_t count);
功能:把指定数目的数据读到内存(缓冲区)
参数:
	fd: 文件描述符
	buf: 内存首地址,是将文件里的内容读出来存放到内存中,因此需要定义对应数据类型的变量或者申请空间来存放
	count: 一次最多读取的字节个数
返回值:
	成功:实际读取到的字节个数
	失败:-1
  • 代码演示
void test04()
{
    // 打开文件
    int fd = open("c.txt", O_RDONLY);

    if (fd < 0)
    {
        perror("open");
        return;
    }
    printf("文件%d打开成功\n", fd);

    // 读取数据
    char buf[32] = "";
    int len = read(fd, buf, 32);
    printf("读取到字节数:len = %d, 读取到的数据:%s\n", len, buf);

    // 关闭文件
    close(fd);
}
  • 运行结果
文件3打开成功
读取到字节数:len = 13, 读取到的数据:hello world!
// 13个字节还包含了换行符

3.3实现 cp 命令

  • 案例:将 c.txt 文件拷贝到 test 目录中
void test05(int argc, char const *argv[])
{
    // 判断终端输入是否正确
    if (argc != 3)
    {
        printf("./a.out 文件名 目标目录\n");
        return;
    }

    // 打开源文件
    int fd_old = open(argv[1], O_RDONLY);
    if (fd_old < 0)
    {
        perror("open");
        return;
    }
    printf("源文件%d打开成功\n", fd_old);

    // 在目的目录创建拷贝文件
    char new_file[32] = "";
    // 组包得到新文件的目录/文件名
    sprintf(new_file, "%s/%s", argv[2], argv[1]);
    int fd_new = open(new_file, O_WRONLY | O_CREAT, 0666);
    if (fd_new < 0)
    {
        perror("open");
        return;
    }
    printf("目标文件%d创建成功\n", fd_new);

    // 循环读取源文件内容写入新文件
    while (1)
    {
        // 每次读取8字节数据,可以多写一点,我这里方便演示,设置了个较小值
        unsigned char buf[8] = "";
        int len = read(fd_old, buf, sizeof(buf));
        // 将读取到的数据写入新文件
        write(fd_new, buf, len);

        // 写完退出
        if (len < sizeof(buf))
        {
            printf("拷贝完成\n");
            break;
        }
    }

    // 关闭文件
    close(fd_old);
    close(fd_new);
}

int main(int argc, char const *argv[])
{
    test05(argc, argv);
    return 0;
}
  • 运行结果
edu@edu:~/study/my_code$ tree
.
├── 01-first_code.sh
├── 02-shell变量.sh
├── 03-file_operator.c
├── a.out
├── a.txt
├── b.txt
├── c.txt
└── test

1 directory, 7 files
edu@edu:~/study/my_code$ cat c.txt
hello world!
hello friend!

edu@edu:~/study/my_code$ gcc 03-file_operator.c
edu@edu:~/study/my_code$ ./a.out
./a.out 文件名 目标目录
edu@edu:~/study/my_code$ ./a.out c.txt test
源文件3打开成功
目标文件4创建成功
拷贝完成
edu@edu:~/study/my_code$ cat test/c.txt
hello world!
hello friend!
  • 说明:
    1. int main(int argc, char const *argv[])中的int argc代表执行可执行文件时传入的参数的个数(包括可执行文件名),char const *argv[]是一个指针数组,存放的是每个参数的首元素地址;
    2. 目的文件为 目的目录/拷贝文件名 的拼接,因此需要先组包,才能确定写入的文件的目录结构;
    3. unsigned char buf[8]用于多次从源文件读取指定字节数据,再写入到目的文件,应定义为无符号类型,因为有符号最大只能读 127 字节长度,有些数据可能超过 127 ,如图片的颜色,白色为 255;
    4. 当读取到的实际字节数没有占满 buf 时,代表已经读取到最后的内容了,可以作为循环结束的条件;
    5. 记得打开几个文件就要关闭几个。

四、文件特性和状态

1.文件的阻塞特性

对于一些设备文件读写操作,如管道、套接字、标准输入输出设备、默认缓冲区没有数据读会带阻塞,默认缓存区满的状态,写也会阻塞。阻塞与非阻塞是对于文件而言的,而不是指 read、write 等的属性。

1.1默认阻塞特性

open 打开文件的时候,默认为阻塞特性,如果不输入数据,进行读操作 read 的时候就阻塞,直到输入数据且按下回车解阻塞。

  • 代码演示:
void test06()
{
    // 打开文件
    int fd = open("/dev/tty", O_RDONLY);

    if (fd < 0)
    {
        perror("open");
        return;
    }
    printf("等待用户输入... ...\n");

    // 读取数据
    char buf[32] = "";
    int len = read(fd, buf, 32);
    printf("读取到字节数:len = %d, 读取到的数据:%s\n", len, buf);

    // 关闭文件
    close(fd);
}
  • 运行结构:
等待用户输入... ...
hello world
读取到字节数:len = 12, 读取到的数据:hello world

  • 说明:
    1. /dev/tty 是终端输入设备,这里通过 open 打开终端输入设备;
    2. 当终端输入设备(键盘)未输入的时候,read 会一直阻塞,当输入并回车的时候,解阻塞,因此可以证明 open 打开的文件默认为阻塞特性。

1.2 open 打开文件设置非阻塞

在打开文件的时候 flag 按位或上 O_NONBLOCK,设置非阻塞特性。

  • 代码演示
void test07()
{
    // 打开文件
    int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);

    if (fd < 0)
    {
        perror("open");
        return;
    }
    printf("等待用户输入... ...\n");

    // 读取数据
    char buf[32] = "";
    int len = read(fd, buf, 32);
    printf("读取到字节数:len = %d, 读取到的数据:%s\n", len, buf);

    // 关闭文件
    close(fd);
}
  • 运行结果
等待用户输入... ... # 用户没有输入就执行
读取到字节数:len = -1, 读取到的数据: # 读取字节数为 -1 代表读取失败
  • 说明:
    1. 可以看到,设置非阻塞后,不管有没有数据输入,在读数据的时候都不会阻塞;
    2. 都是通过 read 读取数据,但同样的方式读取数据,可以阻塞也可以不阻塞,说明阻塞不是 read、write 的特性,而是文件的特性,只是文件在遇到 read、write 等操作的时候,该特性才显现出来。

1.3 fcntl 设置文件非阻塞

上面设置文件非阻塞是在文件未打开的情况下,我们通过 open 打开的时候去设置,但如果文件已经打开了那可以通过 fcntl 函数去设置。

  • 代码演示:以标准输入设备为例,已打开的文件默认阻塞
void test08()
{
    printf("等待用户输入... ...\n");

    // 读取数据
    char buf[32] = "";
    int len = read(0, buf, 32);
    printf("读取到字节数:len = %d, 读取到的数据:%s\n", len, buf);
}
  • 运行结果
等待用户输入... ...
hello world
读取到字节数:len = 12, 读取到的数据:hello world
  • 说明:可以看到已经打开的文件默认也是遇到 read 阻塞。

  • fcntl 函数介绍

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
功能:改变已打开的文件性质,fcntl针对描述符提供控制
参数:
	fd:操作的文件描述符
	cmd:操作方式
	arg:针对 cmd 的值,fcntl 能够接受第三个参数 int arg
返回值:
	成功:返回某个其他值
	失败:-1
fcntl 函数有 5 种功能:
1.复制一个现有的描述符(cmd=F_DUPFD)
2.获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)
3.获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL) # 设置非阻塞用这个文件状态标记
4.获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)
5.获得/设置记录锁(cmd=F_GETLK, F_SETLK 或 F_SETLKW)
  • 代码演示
void test09()
{
    // 定义一个变量,用于接收文件状态标记
    int flags = 0;
    flags = fcntl(0, F_GETFL);
    // 修改文件状态标记
    flags |= O_NONBLOCK;
    // 设置文件状态为非阻塞
    fcntl(0, F_SETFL, flags);

    printf("等待用户输入... ...\n");
    // 读取数据
    char buf[32] = "";
    int len = read(0, buf, 32);
    printf("读取到字节数:len = %d, 读取到的数据:%s\n", len, buf);
}
  • 运行结果
等待用户输入... ...
读取到字节数:len = -1, 读取到的数据:
  • 说明:
    1. 设置已打开文件的非阻塞特性,要先获取文件当前的文件状态标记;
    2. 将文件状态标记按位或上 O_NONBLOCK ,加上非阻塞状态。为什么是按位或,因为按位或遇0不变,遇1置1,即把要添加的特性部位置为1,通过按位或上特定的 flag 就行;
    3. 最后为文件设置添加非阻塞特性后的状态标记。

2.文件的状态

2.1获取文件状态的方法

  • 函数介绍
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
功能:获取文件状态信息
参数:
	path:文件名
	buf:保存文件信息的结构体
返回值:
	成功: 0
	失败: -1
  • 说明:上面有两函数,当文件是一个符号链接时,lstat 返回的是该符号链接本身的信息;而 stat 返回的是该链接指向的文件的信息。

  • 保存文件信息的结构体介绍,可以通过 man 2 stat查看相关信息:

struct stat {
    dev_t st_dev; //文件的设备编号
    ino_t st_ino; //节点
    mode_t st_mode; //文件的类型和存取的权限
    nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为 1
    uid_t st_uid; //用户 ID
    gid_t st_gid; //组 ID
    dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
    off_t st_size; //文件字节数(文件大小)
    blksize_t st_blksize; //块大小(文件系统的 I/O 缓冲区大小)
    blkcnt_t st_blocks; //块数
    time_t st_atime; //最后一次访问时间
    time_t st_mtime; //最后一次修改时间
    time_t st_ctime; //最后一次改变时间(指属性)
};
// 不需要记住,只需要用到的时候会查询就行
  • st_mode 16位参数介绍:

st_mode 一共16位,从低到高,02为其它用户权限,35为同组用户权限,68为所有者用户权限,1215为文件类型,如图:

在这里插入图片描述

按位与操作:遇到 1 保持不变,遇到 0 置0,比如我们想要知道所有者是否具有写权限时:

st_mode & 0000 0000 1000 0000(八进制为:00200)

如果结果为 00200 ,代表 st_mode 中对应位为1,具备可写权限,否则不具备。

不过实际开发中,不需要我们这么麻烦去找对应位置,系统已经提前设置好了对应的宏,可以直接对宏按位与操作来判断。

  • 用户权限的宏
// 所有者用户权限
S_IRWXU     00700   过滤 st_mode 中除所有者用户权限以外信息
S_IRUSR     00400   owner has read permission 读
S_IWUSR     00200   owner has write permission 写
S_IXUSR     00100   owner has execute permission 执行

S_IRWXG     00070   过滤 st_mode 中除同组用户权限以外信息
S_IRGRP     00040   group has read permission 读
S_IWGRP     00020   group has write permission 写
S_IXGRP     00010   group has execute permission 执行

S_IRWXO     00007   过滤 st_mode 中除其它用户权限以外信息
S_IROTH     00004   others have read permission 读
S_IWOTH     00002   others have write permission 写
S_IXOTH     00001   others have execute permission 执行
  • 文件类型的宏
S_IFMT     0170000   过滤 st_mode 中除文件类型掩码以外的信息

S_IFSOCK   0140000   socket套接字
S_IFLNK    0120000   symbolic link软连接
S_IFREG    0100000   regular file普通文件
S_IFBLK    0060000   block device块设备
S_IFDIR    0040000   directory目录
S_IFCHR    0020000   character device字符设备
S_IFIFO    0010000   FIFO管道

// 判断方法
stat(pathname, &sb);
if ((sb.st_mode & S_IFMT) == S_IFREG) 
// 通过获取到的 st_mode 按位与 S_IFMT 过滤宏,得到对应类型文件的宏
{
	/* Handle regular file */
}

// 为了方便,也可以使用如下的宏函数判断文件类型
S_ISREG(m)  is it a regular file?
S_ISDIR(m)  directory?
S_ISCHR(m)  character device?
S_ISBLK(m)  block device?
S_ISFIFO(m) FIFO (named pipe)?
S_ISLNK(m)  symbolic link?  (Not in POSIX.1-1996.)
S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)

// 宏函数使用方法
stat(pathname, &sb);
if (S_ISREG(sb.st_mode))
{
	/* Handle regular file */
}

2.2获取文件状态演示

  • 官方的方法:
// 判断方法
stat(pathname, &sb);
if ((sb.st_mode & S_IFMT) == S_IFREG) 
// 通过获取到的 st_mode 按位与 S_IFMT 过滤宏,得到对应类型文件的宏
{
	/* Handle regular file */
}
  • 说明:

    1. 官方的方法就是先通过 stat 函数获取文件状态信息;
    2. 然后通过文件状态按位与上过滤宏(sb.st_mode & S_IFMT),其实就是将文件状态中和文件类型不相关的位置0,只和相关位按位与操作,得到的结果与不同类别文件的宏比较;
    3. 过滤非文件类型宏的二进制写法:001 111 000 000 000 000 ,除了12~15位为1,其它全置0,转化为八进制就是 0170000,即S_IFMT;
    4. 但是我们看文件类别对应的宏,如S_IFSOCK 0140000除了12~15位,其它位本身就全为0,因此可以省略掉过滤,直接 st_mode 与文件类别对应的宏按位与操作,如if ((sb.st_mode & S_IFREG) == S_IFREG),文件权限也同理。
  • 代码演示:

void test10()
{
    // 创建一个 stat 结构体变量
    struct stat file_stat;
    // 获取文件状态
    stat("c.txt", &file_stat);

    // 判断文件类型
    if (S_ISDIR(file_stat.st_mode))
        printf("是一个目录\n");
    if (S_ISREG(file_stat.st_mode))
        printf("是一个文件\n");

    // 判断所有者权限
    if ((file_stat.st_mode & S_IRUSR) == S_IRUSR)
        printf("所有者有可读权限\n");
    if ((file_stat.st_mode & S_IWUSR) == S_IWUSR)
        printf("所有者有可写权限\n");
    if ((file_stat.st_mode & S_IXUSR) == S_IXUSR)
        printf("所有者有可执行权限\n");

    // 查看文件的大小
    printf("文件占%ld个字节\n", file_stat.st_size);
}
  • 运行结果
edu@edu:~/study/my_code$ ls c.txt -lh
-rwxrwxr-x 1 edu edu 28 12月 30 11:03 c.txt
edu@edu:~/study/my_code$ ./a.out
是一个文件
所有者有可读权限
所有者有可写权限
所有者有可执行权限
文件占28个字节

五、目录操作

目录操作主要是为了知道指定目录下有多少个文件和目录。

1. 打开关闭目录

1.1 opendir 打开目录

  • 函数介绍
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
功能:打开一个目录
参数:
	name:目录名
返回值:
	成功:返回指向该目录结构体指针
	失败:NULL

1.2 closedir 关闭目录句柄

句柄也就是结构体指针的意思。

  • 函数介绍
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
功能:关闭目录
参数:
	dirp:opendir 返回的结构体指针
返回值:
	成功:0
	失败:-1

2.从目录句柄读文件

2.1函数介绍

  • readdir 函数语法
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
功能:读取目录
参数:
	dirp:opendir 的返回值
返回值:
	成功:目录或文件等的结构体指针
	失败:NULL
  • struct dirent 结构体介绍
struct dirent
{
    ino_t d_ino; // 此目录进入点的 inode
    off_t d_off; // 目录文件开头至此目录进入点的位移
    signed short int d_reclen; // d_name 的长度,不包含 NULL 字符
    unsigned char d_type; // d_type 所指的文件类型
    char d_name[256]; // 文件名
};
  • 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 is unknown. 不知道的文件类型

用法和前面文件状态一样,d_type按位与上对应的宏来判断,也可以直接 == 判单,推荐 == 判断。

2.2扫描当前层目录

  • 扫描当前目录文件
void test11()
{
    // 打开目录
    DIR *p = opendir("./");
    if (NULL == p)
    {
        perror("opendir");
        return;
    }

    // 逐个读取文件分析类型
    while (1)
    {
        struct dirent *ret = readdir(p);
        // 当返回值为NULL代表读取失败,已经读完
        if (NULL == ret)
        {
            printf("目录扫描完成\n");
            break;
        }
        if ((ret->d_type & DT_DIR) == DT_DIR)
        {
            printf("%s是目录\n", ret->d_name);
        }
        else if ((ret->d_type & DT_REG) == DT_REG)
        {
            printf("%s是文件\n", ret->d_name);
        }
    }

    // 关闭句柄
    closedir(p);
}
  • 运行结果
.是目录
02-shell变量.sh是文件
a.out是文件
03-file_operator.c是文件
01-first_code.sh是文件
a.txt是文件
.vscode是目录
test是目录
b.txt是文件
..是目录
c.txt是文件
目录扫描完成

2.3扫描当前目录所有层

即能够像 tree 命令一样看到目录里的目录。

  • 代码演示
void scan_dir(char *path)
{
    // 定义字符数组,用于保存要扫描的目录名
    char file_dir[128] = "";
    strcpy(file_dir, path);

    // 1、打开目录
    DIR *dir = opendir(file_dir);
    if (NULL == dir)
    {
        perror("opendir");
        return;
    }

    // 2、扫描目录中的文件
    while (1)
    {
        struct dirent *dirent = readdir(dir);
        if (NULL == dirent)
            break;

        // 判断传入的类型:是否为目录,且不是当前目录和上一级目录
        if (dirent->d_type == DT_DIR && strcmp(dirent->d_name, ".") != 0 && strcmp(dirent->d_name, "..") != 0)
        {
            char new_dir[32] = "";
            // 如果是当前目录,拼接的时候不加 /
            if (strcmp(file_dir, "./") == 0)
                sprintf(new_dir, "%s%s", file_dir, dirent->d_name);
            // 如果不是当前目录,那只能是目录里面的目录,拼接的时候加 /,只是为了好看
            else
                sprintf(new_dir, "%s/%s", file_dir, dirent->d_name);
            // 将组包后的路径名传入,递归调用函数
            scan_dir(new_dir);
        }
        // 如果不是目录,则判断是否是文件,因为除了目录,可能是. 和..,因此还得判断是否为文件
        else if (dirent->d_type == DT_REG)
        {
            if (strcmp(file_dir, "./") == 0)
                printf("%s%s 是普通文件\n", file_dir, dirent->d_name);
            else
                printf("%s/%s 是普通文件\n", file_dir, dirent->d_name);
        }
    }
    // 关闭目录句柄
    closedir(dir);
}

void test12()
{
    scan_dir("./");
}
  • 运行结果
./02-shell变量.sh 是普通文件
./a.out 是普通文件
./03-file_operator.c 是普通文件
./01-first_code.sh 是普通文件
./a.txt 是普通文件
./.vscode/c_cpp_properties.json 是普通文件
./test/c.txt 是普通文件
./b.txt 是普通文件
./c.txt 是普通文件
;