
01_文件编程
本文最后更新于 2025-01-07,学习久了要注意休息哟
1. 系统编程简介
1.1 什么是系统编程
Linux系统编程是指编写在Linux操作系统上运行的程序,这些程序可以直接与Linux内核进行交互,利用操作系统提供的各种系统调用和库函数来实现各种功能。Linux系统编程通常需要深入了解Linux操作系统的内部机制和实现原理,因为它需要开发人员能够利用系统调用、线程、进程、信号、文件I/O等底层机制来编写高效、稳定和可靠的程序。
在Linux系统编程中,开发人员可以利用各种编程语言(如C、C++、Python等)和开发工具来编写程序,包括编辑器、调试器、编译器等。Linux系统编程可以用于各种领域,如网络编程、系统管理、嵌入式系统、安全等。
1.2 Linux系统分层
用户空间:用户空间是Linux系统的最高层,包括所有用户进程和应用程序。在这一层中,用户可以运行各种软件,例如文本编辑器、浏览器、图像处理软件等。此层还包括用户可以访问的文件系统和各种系统服务和工具。
系统调用接口:系统调用接口是Linux内核与用户空间之间的接口层。在这一层中,应用程序使用系统调用来向内核发送请求,以获取系统资源或执行特定操作。系统调用接口提供了一组标准的系统调用,例如读写文件、创建进程和管理进程等。
内核空间:内核空间是Linux系统的中间层,它包括内核代码和数据结构。在这一层中,内核提供了各种服务和功能,例如进程管理、文件系统管理、网络管理和设备驱动程序等。此层还包括内核的各种子系统和模块,例如内存管理、调度程序、安全模块等。
硬件层:硬件层是Linux系统的最底层,它由各种物理设备组成,例如CPU、内存、磁盘、网络接口卡等。在这一层中,内核使用设备驱动程序与硬件设备进行交互,以控制和管理它们。
1.3 POSIX 标准
POSIX(Portable Operating System Interface,可移植操作系统接口)是由IEEE和Open Group联合制定的一系列标准,旨在定义一组操作系统接口,使得不同的操作系统之间可以实现可移植性。 POSIX标准主要包括以下几个方面:
- 系统调用:定义了一组标准的系统调用接口,包括文件操作、进程管理、信号处理等。
- 库函数:定义了一组标准的库函数接口,包括数学函数、字符串操作函数、时间函数等。
- 命令行接口:定义了一组标准的命令行接口,包括命令行选项、环境变量、标准输入输出等。
- 线程接口:定义了一组标准的线程接口,包括线程创建、同步和销毁等。
POSIX标准被广泛应用于Unix和类Unix操作系统中,如Linux、macOS等。通过使用POSIX标准,开发者可以写出与具体操作系统无关的程序,提高了程序的可移植性和可重用性。
1.4 系统调用 与 用户程序编程接口
1.4.1 系统调用
操作系统管理计算机资源,并通过系统调用接口向用户程序提供服务,如分配内存、创建进程、进程通信等。程序不能直接访问硬件资源,因为这会导致安全和稳定性问题。在嵌入式系统中,操作系统管理资源,程序通过系统调用请求操作系统提供的服务。不同操作系统的系统调用接口各不相同,Linux 的系统调用接口简洁并继承了 UNIX 的核心功能,如进程控制、文件系统管理、网络管理等。
1.4.2 用户程序编程接口
虽然系统调用接口能提供基本功能,但实际开发中,程序员通常使用更高层的用户程序编程接口(API),如 C 库中的函数。这些 API 更加便捷和通用,具备以下优势:
- 提供的功能更丰富,满足开发需求。
- 兼容性好,API 使程序更易移植到不同操作系统。
API 在实现功能时通常依赖于系统调用接口,例如 fork()
对应 sys_fork()
系统调用。在 Linux 中,API 遵循 POSIX 标准,通过 C 库 (libc) 实现,这确保了程序在多种操作系统上的兼容性和可移植性。
2. 标准IO
2.1 标准IO概述
标准输入输出(Standard I/O,简称标准IO)是 C 语言中最基础的库函数之一,用于程序与外部环境进行数据交互。标准 I/O 函数提供了统一的接口,简化了用户输入和程序输出的操作。标准输入和输出通常通过文件流进行处理,包括从键盘读取输入,向屏幕输出,或是进行文件操作等。
2.1.1 标准IO
在 C 语言中,标准 I/O 库主要涉及三个预定义的文件流:
- 标准输入(stdin):用于从外部输入获取数据,通常是从键盘。程序使用
scanf()
函数或fgets()
函数从标准输入流读取数据。 - 标准输出(stdout):用于将数据输出到外部,通常是显示在屏幕上。程序使用
printf()
函数将数据输出到标准输出流。 - 标准错误(stderr):用于输出错误信息,与标准输出流分开。错误输出通常用于调试,避免混淆正常的程序输出。程序使用
fprintf(stderr, ...)
或perror()
函数输出错误信息。
这三个文件流在 C 语言中是默认预定义的,程序员无需显式地打开文件。标准 I/O 提供了便利的接口,使得用户能够简化输入输出操作。
2.1.2 流的含义
在 C 语言中,“流”是指数据的传输路径。流可以分为两类:
- 输入流:从外部设备读取数据。典型的输入流是从键盘或文件中获取数据,常通过
scanf()
、fscanf()
或fgets()
等函数进行读取。 - 输出流:向外部设备写入数据。典型的输出流是向屏幕或文件输出数据,常通过
printf()
、fprintf()
或fputs()
等函数进行写入。
流的本质是一个传输数据的通道,程序通过这些通道与外部设备进行交互。每个流都有一个缓冲区,用来存储待输入或待输出的数据,以提高 I/O 操作的效率。
2.1.3 流的缓冲区
在 C 语言的标准 I/O 中,缓冲区是用于临时存储数据的内存区域。在进行 I/O 操作时,数据首先存入缓冲区,然后才会被实际读写到外部设备中。缓冲区的主要作用是提高 I/O 操作的效率,避免频繁的磁盘或终端操作。
标准 I/O 库的缓冲机制通常包括以下几种:
- 全缓冲(Fully Buffered):数据在缓冲区中存储,当缓冲区满或显式刷新时,数据才会写入外部设备。标准输出流(stdout)默认使用全缓冲,通常在输出较大数据时提高效率。
- 行缓冲(Line Buffered):数据按行存储,遇到换行符时,缓冲区会自动刷新并将数据输出。标准输入流(stdin)和部分终端输出流通常使用行缓冲。
- 无缓冲(Unbuffered):每次 I/O 操作都会立即访问外部设备,不通过缓冲区。标准错误流(stderr)通常采用无缓冲方式,确保错误信息立即输出。
缓冲区的大小和刷新策略可以通过 C 标准库函数来控制,常见的函数如 setvbuf()
可用于设置缓冲区模式和大小。合理使用缓冲区可以有效提高程序的性能。
2.2 文件操作
2.2.1 文件打开/关闭
文件的打开
函数介绍
FILE *fopen(const char *path, const char *mode);
功能:
参数:
@path 文件路径,指定文件的名称及其位置。
@mode 文件打开模式,指定文件操作类型。
返回值:
成功 返回一个指向 `FILE` 结构的指针,
失败 返回 `NULL`。 重置错误码
打开方式
"r" 打开一个已存在的文件进行只读操作。文件必须存在。
"w" 打开一个文件进行写入操作(如果文件存在,则清空文件内容;如果文件不存在,则创建新文件)。
"a" 打开一个文件进行写入操作(如果文件存在,则追加到文件末尾;如果文件不存在,则创建新文件)。
"r+" 打开一个已存在的文件进行读写操作。文件必须存在。
"w+" 打开一个文件进行读写操作(如果文件存在,则清空文件内容;如果文件不存在,则创建新文件)。
"a+" 打开一个文件进行读写操作(如果文件存在,则追加到文件末尾;如果文件不存在,则创建新文件)。
"rb" 以二进制方式打开文件进行只读操作。
"wb" 以二进制方式打开文件进行写入操作。
"ab" 以二进制方式打开文件进行追加操作。
关闭文件
函数介绍
#include <stdio.h>
int fclose(FILE *stream);
功能:
关闭已打开的文件,释放相关资源,并刷新缓冲区内容(如有必要)。
参数:
@stream: 需要关闭的文件指针。
返回值:
成功 返回 0,
失败 返回 EOF(通常为 -1),并设置相应的错误码。
2.2.2 字符读写
字符读取
#include <stdio.h>
int fgetc(FILE *stream);
功能:
从文件流中读取一个字符,并将文件指针向后移动。
参数:
@stream: 文件指针。
返回值:
成功 返回读取到的字符(以 `int` 形式),
失败或到达文件末尾 返回 `EOF`。
字符写入
#include <stdio.h>
int fputc(int c, FILE *stream);
功能:
将一个字符写入文件流,并将文件指针向后移动。
参数:
@c: 要写入的字符(以 `int` 形式传递)。
@stream: 文件指针。
返回值:
成功 返回写入的字符(以 `int` 形式),
失败 返回 `EOF`。
2.2.3 字符串读写
字符串读取
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
功能:
从文件流中读取最多 `size-1` 个字符,并存储到字符串 `s` 中,直到遇到换行符或文件末尾为止。`fgets` 会自动在字符串末尾添加空字符。
参数:
@s: 存储读取内容的字符数组。
@size: 要读取的最大字符数(包括空字符)。
@stream: 文件指针。
返回值:
成功 返回 `s`,
失败或到达文件末尾 返回 `NULL`。
字符串写入
#include <stdio.h>
int fputs(const char *s, FILE *stream);
功能:
将字符串 `s` 写入文件流,不包括末尾的空字符。
参数:
@s: 要写入的字符串。
@stream: 文件指针。
返回值:
成功 返回一个非负值,
失败 返回 `EOF`。
2.2.4 格式化读写
格式化读取
#include <stdio.h>
int fscanf(FILE *stream, const char *format, ...);
功能:
从文件流中按照指定格式读取数据,并将结果存储到相应的变量中。
参数:
@stream: 文件指针。
@format: 格式化字符串,定义要读取的数据格式。
@...: 一个或多个指向存储读取结果的指针。
返回值:
成功 返回成功读取并赋值的项数,
失败或到达文件末尾 返回 `EOF`。
格式化写入
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);
功能:
按照指定格式将数据写入文件流。
参数:
@stream: 文件指针。
@format: 格式化字符串,定义要写入的数据格式。
@...: 一个或多个要写入的数据。
返回值:
成功 返回写入的字符数,
失败 返回负值。
2.2.5 二进制读写
二进制读取
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:
从文件流中读取 `nmemb` 个数据块,每个数据块的大小为 `size` 字节,并存储到 `ptr` 指向的内存中。
参数:
@ptr: 指向存储读取数据的内存地址。
@size: 每个数据块的大小(以字节为单位)。
@nmemb: 要读取的数据块的数量。
@stream: 文件指针。
返回值:
成功 返回实际读取到的数据块数量
失败或到达文件末尾 返回值小于 `nmemb`。
二进制写入
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:
将 `ptr` 指向的内存数据写入文件流,总共写入 `nmemb` 个数据块,每个数据块的大小为 `size` 字节。
参数:
@ptr: 指向要写入的数据的内存地址。
@size: 每个数据块的大小(以字节为单位)。
@nmemb: 要写入的数据块的数量。
@stream: 文件指针。
返回值:
成功 返回实际写入的数据块数量,
失败 返回值小于 `nmemb`。
2.2.6 文件定位
文件指针定位
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
功能:
根据指定的偏移量和基准位置,移动文件指针。
参数:
@stream: 文件指针。
@offset: 相对于 `whence` 的偏移量(以字节为单位)。
@whence: 基准位置,可以是以下值之一:
- `SEEK_SET`: 从文件开头开始偏移。
- `SEEK_CUR`: 从当前文件指针位置开始偏移。
- `SEEK_END`: 从文件末尾开始偏移。
返回值:
成功 返回 0,
失败 返回 -1。
获取文件指针位置
#include <stdio.h>
long ftell(FILE *stream);
功能:
获取当前文件指针的位置(以字节为单位)。
参数:
@stream: 文件指针。
返回值:
成功 返回当前文件指针的偏移量
失败 返回 -1。
重置文件指针到开头
#include <stdio.h>
void rewind(FILE *stream);
功能:
将文件指针重置到文件开头,并清除文件流的错误和文件结束状态。
参数:
@stream: 文件指针。
返回值:
无返回值。
2.2.7 错误处理
标准 I/O 函数执行时如果出现错误,会把错误码保存在全局变量 errno 中。程序员可以通过相应的函数打印错误信息。
错误码
int errno;
功能: `errno` 是一个全局变量,用于存储最近一次系统调用或库函数的错误码。当某个函数调用失败时,通常会设置 `errno` 为相应的错误码。
输出错误信息
#include <errno.h>
void perror(const char *s);
功能:
返回一个指向错误码描述信息的指针。这个描述信息是一个以空字符结尾的字符串,可以用于打印或记录错误信息。
参数:
@errnum: 错误码,一般为 errno 的值。
返回值:
指向错误描述的字符串指针。
返回错误信息
#include <errno.h>
const char *strerror(int errnum);
功能:
返回一个指向错误码描述信息的指针。这个描述信息是一个以空字符结尾的字符串,可以用于打印或记录错误信息。
参数:
@errnum: 错误码,一般为 `errno` 的值。
返回值:
指向错误描述的字符串指针。
2.2.7 缓冲区操作
设置缓冲区
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
功能:
设置文件流的缓冲模式和缓冲区大小。
参数:
@stream: 文件指针。
@buf: 用户提供的缓冲区指针。如果为 `NULL`,则使用系统默认缓冲区。
@mode: 缓冲模式,可以是以下值之一:
- `_IOFBF`: 全缓冲模式,数据在缓冲区填满后才写入文件。
- `_IOLBF`: 行缓冲模式,遇到换行符或缓冲区满时写入数据。
- `_IONBF`: 无缓冲模式,数据直接写入文件或从文件读取。
@size: 缓冲区的大小(以字节为单位)。
返回值:
成功 返回 0,
失败 返回非零值。
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
功能:
设置文件流的缓冲区。与 `setvbuf` 不同,`setbuf` 只能设置全缓冲或无缓冲模式。
参数:
@stream: 文件指针。
@buf: 用户提供的缓冲区指针。如果为 `NULL`,则设置为无缓冲模式。
返回值:
无返回值。
刷新缓冲区
#include <stdio.h>
int fflush(FILE *stream);
功能:
强制将输出流缓冲区中的数据写入文件或刷新输入流缓冲区(如有必要)。
参数:
@stream: 文件指针。如果为 `NULL`,则刷新所有输出流。
返回值:
成功 返回 0,
失败 返回 EOF。
2.3 文件实战
2.3.1 文件的复制
2.3.2 文件的合并
2.3.3 统计文件中的字符、单词和行数
2.3.4 循环记录系统时间
3. 文件IO
3.1 文件IO概述
I/O是指输入/输出(Input/Output)的缩写,在Linux系统中,所有的输入和输出都是通过文件进行的,这也被称为文件I/O。 Linux文件I/O指的是在Linux操作系统中主存和外部设备(比如硬盘、U盘)进行文件输入和输出的过程,其中数据从设备到内存的过程称为输入,数据从内存到设备的过程叫输出。
3.1.1 文件IO与标准IO
标准I/O(stdio):标准I/O函数库提供了一系列高级的文件I/O函数,如fopen、fclose、fread、fwrite等,它们可以帮助程序员更方便地进行文件操作。标准I/O函数库还提供了缓冲区机制,可以提高文件I/O的效率。
文件I/O(syscall):系统调用I/O是直接使用系统调用进行文件操作,例如open、read、write、close等函数。与标准I/O函数库不同,系统调用I/O函数没有缓冲区,文件I/O的效率更高。
原始I/O(raw I/O):原始I/O是一种低级别的文件I/O,使用read和write函数进行数据读写,可以更加细粒度地控制文件的I/O操作。原始I/O一般用于对特殊设备进行操作,例如磁盘分区、网络设备等等。 这三种方式在Linux系统中都可以使用,选择哪一种方式主要取决于具体的应用场景和需要。标准I/O适用于大多数文件I/O操作,可以提高程序的开发效率;系统调用I/O适用于对文件进行更底层的操作,可以提高文件I/O的效率;原始I/O则适用于对特殊设备进行操作。
3.1.2 文件描述符
对于文件I/O来说,一切都是通过文件描述符进行的。在Linux系统中,当打开或者创建一个文件的时候,内核向进程返回一个对应的文件描述符(非负整数)。同时还规定一个进程启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4......
POSIX 定义了 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 来代替 0、1、2。这三个符号常量的定义位于头文件 unistd.h。
通常,一个进程启动时会自动打开三个流:
- 标准输入(文件描述符 0,对应宏
STDIN_FILENO
) - 标准输出(文件描述符 1,对应宏
STDOUT_FILENO
) - 标准错误(文件描述符 2,对应宏
STDERR_FILENO
)
基于文件描述符的 I/O 操作尽管无法直接移植到非类 Linux 系统(如 Windows),但在 Linux 下是实现低层文件操作、I/O 多路复用、TCP/IP 套接字编程等的唯一方式。这些操作兼容 POSIX 标准,方便移植到任何 POSIX 平台上。因此,熟练掌握基于文件描述符的 I/O 操作在 Linux 编程中至关重要。
3.2 文件操作
3.2.1 文件打开/关闭
文件的打开
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.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`: 读写模式。
- `O_CREAT`: 如果文件不存在则创建文件。
- `O_TRUNC`: 如果文件存在并且以写模式打开,则清空文件内容。
- `O_APPEND`: 追加模式,写入的数据添加到文件末尾。
@mode: 当使用 `O_CREAT` 标志时指定文件权限(如 `0644`)但是需要考虑文件掩码的问题。
返回值:
成功 返回文件描述符(非负整数)
失败 返回 -1,并设置相应的错误码。
文件的关闭
#include <unistd.h>
int close(int fd);
功能:
关闭文件描述符,释放相关资源。
参数:
@fd: 要关闭的文件描述符。
返回值:
成功 返回 0,
失败 返回 -1,并设置相应的错误码。
示例程序
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("Error opening file");
return 1;
}
// 文件操作代码...
if (close(fd) == -1) {
perror("Error closing file");
return 1;
}
return 0;
}
3.2.2 文件读取/写入
文件读取
#include <unistd.h>
ssize_t read(int fd, void * buf , size_t count);
功能:
从文件描述符 `fd` 对应的文件中读取最多 `count` 个字节的数据,并存储到 `buf` 指向的缓冲区中。
参数:
@fd: 文件描述符。
@buf: 存储读取数据的缓冲区指针。// 不但可以操作 文本 还可以操作结构体
@count: 需要读取的最大字节数。
返回值:
成功 返回读取的字节数(可能小于 `count`),
失败 返回 -1,并设置相应的错误码。
如果在文件末尾,返回 0。
文件写入
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
功能:
将 `buf` 缓冲区中的 `count` 个字节写入文件描述符 `fd` 对应的文件中。
参数:
@fd: 文件描述符。
@buf: 包含待写入数据的缓冲区指针。
@count: 要写入的字节数。
返回值:
成功 返回实际写入的字节数(可能小于 `count`)
失败 返回 -1,并设置相应的错误码。
3.2.3 文件偏移
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
功能:
改变文件描述符 `fd` 对应文件的指针位置,允许在文件中任意位置进行读写操作。
参数:
@fd: 文件描述符。
@offset: 相对于基准位置的偏移量(可以为正数或负数)。
@whence: 基准位置,可以是以下值之一:
- `SEEK_SET`: 从文件开头开始偏移。
- `SEEK_CUR`: 从当前文件指针位置开始偏移。
- `SEEK_END`: 从文件末尾开始偏移。
返回值:
成功 返回新的文件偏移量
失败 返回 -1,并设置相应的错误码。
3.2.4 示例程序
#include <sys_head.h>
int main(int argc, char const *argv[])
{
// 打开文件
int fd = open( "HQYJ.txt" , O_CREAT | O_RDWR | O_TRUNC , 0666 );
if (EOF == fd )
{
ERRLOG("open");
return -1;
}
char buf[128] = "ssize_t read(int fd, void *buf, size_t count);\n";
// 文件写入
write( fd , buf , strlen(buf));
write( fd , buf , strlen(buf));
write( fd , buf , strlen(buf));
write( fd , buf , strlen(buf));
lseek(fd , 0 , SEEK_SET);
// 文件读取
while ( 0 != read(fd , buf , sizeof(buf) - 1) )
{
printf("%s" , buf);
memset(buf , 0 , sizeof(buf));
}
// 关闭文件
close(fd);
return 0;
}
2.3 文件属性
在 Linux 中,可以使用一系列系统调用来获取和设置文件的属性,包括文件的大小、权限、所有者等。这些系统调用提供了对文件元数据的详细控制。
2.3.1 查看文件属性
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);
功能:
获取指定文件的状态信息(如文件类型、权限、大小等),并将其存储在 `struct stat` 结构体中。
参数:
const char *pathname: 要获取状态信息的文件路径。
struct stat *buf: 指向存储文件状态信息的结构体 `stat`。
返回值:
成功: 返回 0。
失败: 返回 -1,并设置 `errno` 以指示错误原因。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);
功能:
获取由文件描述符 `fd` 指向的文件的状态信息(如文件类型、权限、大小等),并将其存储在 `struct stat` 结构体中。
参数:
int fd: 文件描述符,指向要获取状态信息的文件。
struct stat *buf: 指向存储文件状态信息的结构体 `stat`。
返回值:
成功: 返回 0。
失败: 返回 -1,并设置 `errno` 以指示错误原因。
2.3.2 文件属性结构体
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
struct stat {
dev_t st_dev; /* 文件所在的设备 ID */
ino_t st_ino; /* 文件的 inode 编号 文件的唯一编号 使用ls -i 可以查看 */
mode_t st_mode; /* 文件的类型和权限 */
nlink_t st_nlink; /* 硬链接的数量 */
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
变量是 stat
结构体中的一个字段,表示文件的类型和权限。它包含了文件的类型信息和对该文件的访问权限。这些信息以位掩码(bitmask)的形式存储,因此可以使用位操作来访问或修改特定的类型或权限。
st_mode
由两部分组成:
- 文件类型:表示文件的类型,如普通文件、目录、符号链接等。
- 文件权限:表示文件的访问权限,如读、写、执行权限。
文件类型
S_IFMT /* 文件类型掩码 */
S_IFREG /* 普通文件 */
S_IFDIR /* 目录文件 */
S_IFCHR /* 字符设备文件 */
S_IFBLK /* 块设备文件 */
S_IFLNK /* 符号链接 */
S_IFIFO /* FIFO(管道)文件 */
S_IFSOCK /* 套接字文件 */
S_ISREG(st_mode) 判断是否为常规文件。
S_ISDIR(st_mode) 判断是否为目录。
S_ISCHR(st_mode) 判断是否为字符设备。
S_ISBLK(st_mode) 判断是否为块设备。
S_ISFIFO(st_mode) 判断是否为 FIFO(管道)。
S_ISLNK(st_mode) 判断是否为符号链接。
S_ISSOCK(st_mode) 判断是否为套接字。
操作示例
// 通过掩码提取文件类型
switch (st.st_mode & S_IFMT) {
case S_IFREG:
printf("这是一个普通文件\n");
break;
case S_IFDIR:
printf("这是一个目录文件\n");
break;
case S_IFLNK:
printf("这是一个符号链接\n");
break;
case S_IFCHR:
printf("这是一个字符设备文件\n");
break;
case S_IFBLK:
printf("这是一个块设备文件\n");
break;
case S_IFIFO:
printf("这是一个FIFO文件\n");
break;
case S_IFSOCK:
printf("这是一个套接字文件\n");
break;
default:
printf("未知文件类型\n");
}
// 通过宏函数判断文件类型
if (S_ISDIR(st.st_mode)) {
printf("这是一个目录文件\n");
}
else if (S_ISREG(st.st_mode)) {
printf("这是一个普通文件\n");
}
else if (S_ISLNK(st.st_mode)) {
printf("这是一个符号链接文件\n");
}
else {
printf("其他类型文件\n");
}
文件权限
S_IRUSR /* 用户读权限 */
S_IWUSR /* 用户写权限 */
S_IXUSR /* 用户执行权限 */
S_IRGRP /* 组读权限 */
S_IWGRP /* 组写权限 */
S_IXGRP /* 组执行权限 */
S_IROTH /* 其他用户读权限 */
S_IWOTH /* 其他用户写权限 */
S_IXOTH /* 其他用户执行权限 */
操作示例
if (st_mode & S_IRUSR) {
printf("文件所有者有读权限\n");
}
if (st_mode & S_IWUSR) {
printf("文件所有者有写权限\n");
}
if (st_mode & S_IXUSR) {
printf("文件所有者有执行权限\n");
}
2.3.3 查看用户和用户组
查看用户
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
功能:
根据用户 ID (uid) 获取该用户的详细信息,并返回一个指向包含用户信息的 `passwd` 结构体的指针。
参数:
uid_t uid: 用户的 ID,根据该 ID 来查找对应的用户信息。
返回值:
成功: 返回一个指向 `passwd` 结构体的指针,该结构体包含用户的详细信息(如用户名、主目录等)。
失败: 返回 `NULL`,并设置 `errno` 以指示错误原因。
结构体
struct passwd {
char *pw_name; /* 用户名 */
char *pw_passwd; /* 用户密码(通常不再使用 显示 X) */
uid_t pw_uid; /* 用户 ID */
gid_t pw_gid; /* 用户所属组 ID */
char *pw_gecos; /* 用户全名或备注息 */
char *pw_dir; /* 用户主目录 */
char *pw_shell; /* 用户默认 Shell */
};
查看用户组
#include <sys/types.h>
#include <grp.h>
struct group *getgrgid(gid_t gid);
功能:
根据组 ID (gid) 获取该组的详细信息,并返回一个指向包含组信息的 `group` 结构体的指针。
参数:
gid_t gid: 组的 ID,根据该 ID 来查找对应的组信息。
返回值:
成功: 返回一个指向 `group` 结构体的指针,该结构体包含组的详细信息(如组名、组成员等)。
失败: 返回 `NULL`,并设置 `errno` 以指示错误原因。
结构体
struct group {
char *gr_name; /* 组名 */
char *gr_passwd; /* 组密码(通常不再使用) */
gid_t gr_gid; /* 组 ID */
char **gr_mem; /* 组成员列表 */
};
2.3.4 设置用户
设置用户
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
功能:
更改指定文件或目录的所有者(owner)和/或所属组(group)。
参数:
const char *pathname: 要更改的文件或目录的路径。
uid_t owner: 新的用户 ID(所有者)。如果为 -1,表示不更改所有者。
gid_t group: 新的组 ID。如果为 -1,表示不更改所属组。
返回值:
成功: 返回 0。
失败: 返回 -1,并设置 `errno` 以指示错误原因。
#include <unistd.h>
int fchown(int fd, uid_t owner, gid_t group);
功能:
更改由文件描述符 `fd` 指向的文件的所有者(owner)和/或所属组(group)。
参数:
int fd: 文件描述符,指向要更改所有者和所属组的文件。
uid_t owner: 新的用户 ID(所有者)。如果为 -1,表示不更改所有者。
gid_t group: 新的组 ID。如果为 -1,表示不更改所属组。
返回值:
成功: 返回 0。
失败: 返回 -1,并设置 `errno` 以指示错误原因。
2.3.4 设置文件权限
#include <sys/stat.h>
#include <unistd.h>
int chmod(const char *pathname, mode_t mode);
功能:
更改文件的权限(读、写、执行)。不需要考虑掩码
参数:
@pathname: 文件路径。
@mode: 权限模式(如 `0644` 表示用户可读写,组和其他人只读)。
返回值:
成功 返回 0,
失败 返回 -1,并设置相应的错误码。
2.4 目录操作
目录操作常见应用
- 遍历目录树:递归读取目录及其子目录的内容。
- 文件批处理:对目录下的所有文件执行批量操作,如重命名、分类、压缩等。
- 备份与归档:遍历目录内容并复制文件,生成目录的备份。
目录操作在文件管理、系统监控、批处理工具开发中非常常见,提供了灵活的文件系统访问和控制方式。
2.4.1 创建目录
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
功能:
创建一个新目录。
参数:
@pathname: 目录路径。
@mode: 目录的权限,表示谁可以访问和修改目录(如 `0755` 表示用户有读写执行权限,组和其他人有读执行权限)。
返回值:
成功 返回 0,
失败 返回 -1,并设置相应的错误码。
2.4.2 删除目录
#include <unistd.h>
int rmdir(const char *pathname);
功能:
删除一个空目录。
参数:
@pathname: 需要删除的空目录的路径。
返回值:
成功 返回 0,
失败 返回 -1,并设置相应的错误码。
2.4.5 打开目录
#include <dirent.h>
DIR *opendir(const char *name);
功能:
打开指定路径的目录,并返回一个指向该目录的指针,以供后续读取目录内容使用。
参数:
const char *name: 目录的路径名,可以是相对路径或绝对路径。
返回值:
成功: 返回一个指向已打开目录的 `DIR` 结构体指针。
失败: 返回 `NULL`,并设置 `errno` 来指示错误原因。
2.4.6 读取目录
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
功能:
读取由 `opendir()` 打开的目录流中的下一个目录条目。
参数:
DIR *dirp: 通过 `opendir()` 返回的目录指针。
返回值:
成功: 返回一个指向 `struct dirent` 结构体的指针,表示下一个目录项的信息。
失败: 如果读取到目录末尾或发生错误,返回 `NULL`。如果返回值为 `NULL`,需要通过 `errno` 判断是到达了目录末尾(`errno` 未被设置)还是发生了错误。
结构体
struct dirent {
ino_t d_ino; /* inode 编号 */
off_t d_off; /* 在目录中的偏移 */
unsigned short d_reclen; /* 目录条目的长度 */
unsigned char d_type; /* 文件类型 */
char d_name[256]; /* 文件名 */
};
文件类型
#define DT_UNKNOWN 0 /* 未知文件类型 */
#define DT_REG 1 /* 常规文件 */
#define DT_DIR 2 /* 目录 */
#define DT_FIFO 3 /* FIFO 文件 */
#define DT_SOCK 4 /* 套接字文件 */
#define DT_LNK 5 /* 符号链接 */
#define DT_BLK 6 /* 块设备文件 */
#define DT_CHR 7 /* 字符设备文件 */
// 判断文件类型
switch (entry->d_type) {
case DT_REG:
printf("文件类型: 常规文件\n");
break;
case DT_DIR:
printf("文件类型: 目录\n");
break;
case DT_LNK:
printf("文件类型: 符号链接\n");
break;
default:
printf("文件类型: 未知\n");
}
2.4.7 关闭目录
#include <dirent.h>
int closedir(DIR *dirp);
功能:
关闭由 `opendir()` 打开的目录流,并释放相关的资源。
参数:
DIR *dirp: 指向要关闭的目录流的指针,通常是由 `opendir()` 返回的。
返回值:
成功: 返回 0。
失败: 返回 -1,并设置 `errno` 来指示错误原因。
2.4.8 遍历操作
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
void list_files(const char *path) {
DIR *dir = opendir(path); // 打开指定路径的目录
if (dir == NULL) {
perror("无法打开目录");
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) { // 读取目录中的每一项
// 忽略 "." 和 ".." 目录
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
// 打印当前目录中的文件或目录名称
printf("文件/目录: %s/%s\n", path, entry->d_name);
}
closedir(dir); // 关闭目录流
}
int main() {
const char *dir_path = "."; // 当前目录
list_files(dir_path); // 只遍历一级目录
return 0;
}
2.4.9 递归操作
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
void list_files(const char *path) {
DIR *dir = opendir(path); // 打开目录
if (dir == NULL) {
perror("无法打开目录");
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) { // 读取目录中的每一项
// 忽略 "." 和 ".." 目录
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
// 获取当前项的完整路径
char full_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
// 打印当前文件或目录
printf("文件/目录: %s\n", full_path);
// 如果是目录,则递归调用
if (entry->d_type == DT_DIR) {
list_files(full_path); // 递归遍历子目录
}
}
closedir(dir); // 关闭目录流
}
int main() {
const char *dir_path = "."; // 从当前目录开始
list_files(dir_path); // 递归遍历目录及子目录
return 0;
}
2.5 文件锁
在多进程环境中,如果多个进程同时访问同一个文件,可能会出现数据竞争或冲突。为了避免这种情况,可以使用文件锁机制。文件锁确保在某个进程对文件进行操作时,其他进程不能对该文件进行冲突操作。Linux 提供了 flock
和 fcntl
两种常用的文件锁机制。
2.5.1 flock
文件锁
#include <sys/file.h>
int flock(int fd, int operation);
功能:
flock 用于对文件描述符 fd 所对应的文件进行加锁或解锁操作。
参数:
fd:文件描述符,指向需要加锁的文件。可以通过 open() 或 creat() 函数获得文件描述符。
operation:锁操作类型,指定加锁或解锁的方式。
operation 的取值:
LOCK_SH:共享锁(读锁)。允许多个进程同时持有共享锁,但不允许其他进程获取排他锁。
LOCK_EX:排他锁(写锁)。获取排他锁后,其他进程不能对文件加锁,无论是共享锁还是排他锁。
LOCK_UN:解锁。解除之前对文件的加锁。
LOCK_NB:非阻塞模式。与其他锁一起使用时,如果无法立即获得锁,flock 会立即返回,而不是阻塞。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno,可以通过 perror 或 strerror 获取详细的错误信息。
示例程序
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
int main() {
int fd = open("example.txt", O_RDWR); // 打开文件
if (fd == -1) {
perror("打开文件失败");
return 1;
}
// 加共享锁
if (flock(fd, LOCK_SH) == -1) {
perror("加共享锁失败");
close(fd);
return 1;
}
printf("共享锁已加上\n");
// 模拟一些文件操作
sleep(5);
// 解锁
if (flock(fd, LOCK_UN) == -1) {
perror("解锁失败");
close(fd);
return 1;
}
printf("锁已释放\n");
// 加排他锁
if (flock(fd, LOCK_EX) == -1) {
perror("加排他锁失败");
close(fd);
return 1;
}
printf("排他锁已加上\n");
// 模拟文件操作
sleep(5);
// 解锁
if (flock(fd, LOCK_UN) == -1) {
perror("解锁失败");
close(fd);
return 1;
}
printf("锁已释放\n");
// 关闭文件
close(fd);
return 0;
}
2.5.1 fcntl
文件锁
#include <fcntl.h>
#include <sys/file.h>
int fcntl(int fd, int cmd, struct flock *lock);
功能:
fcntl 函数可以用于对文件描述符 fd 所指向的文件进行锁定、解锁操作,并且能够获取或修改文件的某些属性。
参数:
fd:文件描述符,指向需要操作的文件。
cmd:控制命令,指定操作的类型。常见的操作包括 F_GETLK、F_SETLK、F_SETLKW 等。
lock:一个指向 `struct flock` 结构体的指针,指定锁的类型、起始位置、长度等信息,或者用来存放锁的状态(在某些命令中,lock 可以为空)。
struct flock
结构体:
struct flock {
short l_type; /* 锁的类型,见下文 */
short l_whence; /* 锁定的起始位置,通常使用 SEEK_SET */
off_t l_start; /* 锁定区间的起始位置 */
off_t l_len; /* 锁定区间的长度,0 表示直到文件末尾 */
pid_t l_pid; /* 锁的持有者的进程 ID */
};
l_type 的常用值:
F_RDLCK:共享锁(读锁)。
F_WRLCK:排他锁(写锁)。
F_UNLCK:解锁。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
int main() {
int fd = open("example.txt", O_RDWR); // 打开文件
if (fd == -1) {
perror("打开文件失败");
return 1;
}
struct flock lock;
lock.l_type = F_WRLCK; // 设置为排他锁(写锁)
lock.l_whence = SEEK_SET; // 锁定从文件开头开始
lock.l_start = 0; // 锁定区间从文件开头开始
lock.l_len = 0; // 锁定整个文件(0 表示直到文件末尾)
lock.l_pid = getpid(); // 当前进程 ID
// 设置锁
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("加锁失败");
close(fd);
return 1;
}
printf("文件已加排他锁\n");
// 模拟一些文件操作
sleep(5);
// 解锁
lock.l_type = F_UNLCK; // 解锁
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("解锁失败");
close(fd);
return 1;
}
printf("文件已解锁\n");
close(fd);
return 0;
}
2.6 文件实战
2.6.1 消费者和生产者
4 库的制作
4.1 库的概念
库是一组已经编译好、可以被多个程序重复使用的函数和代码集合。使用库可以避免代码重复,提高开发效率。库主要分为两种:静态库和动态库。
4.2.1 静态库
静态库是编译时就会被打包到目标程序中的库。使用静态库时,库中的代码在编译时直接被复制到生成的可执行文件中。这意味着静态库会让生成的可执行文件变得更大,但运行时不需要依赖外部的库文件。
静态库特点:
- 文件扩展名:在 Linux 中通常以
.a
作为后缀(如libmylib.a
)。 - 链接方式:编译时将库直接整合到可执行文件中,生成的可执行文件是完全独立的。
- 优点:部署简单,执行时不依赖外部库,不会因为库文件丢失而无法运行。
- 缺点:程序文件较大,更新库需要重新编译所有依赖该库的程序。
4.2.2 动态库
动态库是在程序运行时才被加载的库。与静态库不同,动态库中的代码不会被直接复制到可执行文件中,而是在运行时由操作系统动态加载。动态库使得多个程序可以共享同一份库代码,从而节省内存和磁盘空间。
动态库特点:
- 文件扩展名:在 Linux 中通常以
.so
作为后缀(如libmylib.so
)。 - 链接方式:在程序运行时由操作系统加载库,多个程序可以共享同一个动态库。
- 优点:节省内存,库的更新不需要重新编译所有依赖该库的程序。
- 缺点:如果库文件丢失或版本不兼容,程序在运行时会报错。
- 静态库在编译时被打包到程序中,生成的可执行文件独立且无需外部依赖,但文件较大。
- 动态库在运行时加载,多个程序可以共享,节省资源,但运行时依赖外部库文件。
两者的选择取决于具体的需求:如果希望程序简单独立且不依赖外部文件,可以选择静态库;如果需要节省资源并方便更新库,则可以选择动态库。
4.2 动态库
4.2.1 条件
静态库文件的名称必须以 lib
开头,文件后缀为 .a
。
例如,如果你的库名是 mylib
,那么静态库文件应命名为 libmylib.a
。这是链接器识别库文件的标准格式。
4.2.2 步骤
1、编译源文件生成目标文件(.o 文件)
gcc -c file1.c file2.c
or
gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
2、使用 ar
命令创建静态库
ar rcs libmylib.a file1.o file2.o
解释:
r
: 插入或更新库中的文件。c
: 创建库(如果库不存在)。s
: 创建库索引,帮助链接器快速查找符号。
4.2.3 调用
gcc -o main main.c -L. -lmylib
-L.
指定库路径为当前目录。
-lmylib
链接 libmylib.a
静态库。
4.3 静态库
4.2.1 条件
动态库文件的名称必须以 lib
开头,文件扩展名为 .so
。
例如,库名为 mylib
,则动态库应命名为 libmylib.so
。
4.2.2 步骤
1、编译源文件生成位置无关代码
使用 -fPIC
选项编译源文件生成位置无关代码(.o
文件):
gcc -fPIC -c file1.c file2.c
or
gcc -fPIC -c file1.c -o file1.o
gcc -fPIC -c file2.c -o file2.o
2、使用 gcc -shared
创建动态库
使用 gcc -shared
选项将目标文件打包成动态库:
gcc -shared -o libmylib.so file1.o file2.o
解释:
-shared
:指示生成动态库。-o libmylib.so
:指定生成的动态库文件名。
生成的动态库文件是 libmylib.so
。
4.2.3 调用
1、编译并链接动态库
在编译时,使用 -L
选项指定库路径,并使用 -l
选项指定库名称(注意省略 lib
前缀和 .so
后缀):
gcc -o main main.c -L. -lmylib
-L.
指定库路径为当前目录。
-lmylib
链接 libmylib.so
动态库。
方式一:临时添加环境变量
-
编译主程序并链接动态库:
gcc main.c -L. -llibmylib
-
设置
LD_LIBRARY_PATH
环境变量:在终端中运行以下命令,将动态库路径添加到
LD_LIBRARY_PATH
中:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
这条命令仅在当前终端会话中有效。关闭终端后,需要重新设置。
-
运行程序:
./a.out
3.3.2 方式二:永久添加到系统库路径
-
将动态库复制到系统库路径(需要root权限):
# 系统库的路径可能是 /lib 或者 /usr/lib sudo cp ./libmylib.so /usr/lib/
这样做会将动态库永久添加到系统库路径中,任何用户和程序都可以直接访问该动态库。
-
编译主程序并链接动态库:
gcc main.c -L /usr/lib/ -llibmylib -o
-
运行程序:
./a.out
3.3.3 方式三:修改配置文件添加库路径
-
修改配置文件,将动态库路径添加到系统库路径配置文件中(需要root权限):
sudo vim /etc/ld.so.conf.d/libc.conf # 在libc.conf文件中添加一行:/path/to/your/lib
-
使配置文件立即生效:
sudo ldconfig
这将更新系统的库缓存,使新增的库路径生效。
-
编译主程序并链接动态库:
gcc main.c -I ../include/ -L /path/to/your/lib -lhqyj -o ../bin/main
-
运行程序:
./a.out
- 感谢你赐予我前进的力量