本文最后更新于 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 更加便捷和通用,具备以下优势:

  1. 提供的功能更丰富,满足开发需求。
  2. 兼容性好,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 由两部分组成:

  1. 文件类型:表示文件的类型,如普通文件、目录、符号链接等。
  2. 文件权限:表示文件的访问权限,如读、写、执行权限。

文件类型

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 提供了 flockfcntl 两种常用的文件锁机制。

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 静态库

静态库是编译时就会被打包到目标程序中的库。使用静态库时,库中的代码在编译时直接被复制到生成的可执行文件中。这意味着静态库会让生成的可执行文件变得更大,但运行时不需要依赖外部的库文件。

静态库特点:

  1. 文件扩展名:在 Linux 中通常以 .a 作为后缀(如 libmylib.a)。
  2. 链接方式:编译时将库直接整合到可执行文件中,生成的可执行文件是完全独立的。
  3. 优点:部署简单,执行时不依赖外部库,不会因为库文件丢失而无法运行。
  4. 缺点:程序文件较大,更新库需要重新编译所有依赖该库的程序。

4.2.2 动态库

动态库是在程序运行时才被加载的库。与静态库不同,动态库中的代码不会被直接复制到可执行文件中,而是在运行时由操作系统动态加载。动态库使得多个程序可以共享同一份库代码,从而节省内存和磁盘空间。

动态库特点:

  1. 文件扩展名:在 Linux 中通常以 .so 作为后缀(如 libmylib.so)。
  2. 链接方式:在程序运行时由操作系统加载库,多个程序可以共享同一个动态库。
  3. 优点:节省内存,库的更新不需要重新编译所有依赖该库的程序。
  4. 缺点:如果库文件丢失或版本不兼容,程序在运行时会报错。

  • 静态库在编译时被打包到程序中,生成的可执行文件独立且无需外部依赖,但文件较大。
  • 动态库在运行时加载,多个程序可以共享,节省资源,但运行时依赖外部库文件。

两者的选择取决于具体的需求:如果希望程序简单独立且不依赖外部文件,可以选择静态库;如果需要节省资源并方便更新库,则可以选择动态库。

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 动态库。

方式一:临时添加环境变量

  1. 编译主程序并链接动态库:

    gcc main.c -L. -llibmylib
    
  2. 设置 LD_LIBRARY_PATH 环境变量:

    在终端中运行以下命令,将动态库路径添加到LD_LIBRARY_PATH中:

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
    

    这条命令仅在当前终端会话中有效。关闭终端后,需要重新设置。

  3. 运行程序:

    ./a.out
    

3.3.2 方式二:永久添加到系统库路径

  1. 将动态库复制到系统库路径(需要root权限):

    # 系统库的路径可能是 /lib 或者 /usr/lib
    sudo cp ./libmylib.so /usr/lib/
    

    这样做会将动态库永久添加到系统库路径中,任何用户和程序都可以直接访问该动态库。

  2. 编译主程序并链接动态库:

    gcc main.c -L /usr/lib/ -llibmylib -o 
    
  3. 运行程序:

    ./a.out
    

3.3.3 方式三:修改配置文件添加库路径

  1. 修改配置文件,将动态库路径添加到系统库路径配置文件中(需要root权限):

    sudo vim /etc/ld.so.conf.d/libc.conf
    # 在libc.conf文件中添加一行:/path/to/your/lib
    
  2. 使配置文件立即生效:

    sudo ldconfig
    

    这将更新系统的库缓存,使新增的库路径生效。

  3. 编译主程序并链接动态库:

    gcc main.c -I ../include/ -L /path/to/your/lib -lhqyj -o ../bin/main
    
  4. 运行程序:

    ./a.out