【Linux】基础IO

【Linux】基础IO

1、C文件IO

1.1 基本IO

C语言中用于与文件进行交互的函数和操作。C文件接口允许程序读取、写入和处理文件的内容。以下是一些常见的C文件接口函数:

  1. fopen:用于打开文件,返回一个文件指针。它的原型是FILE* fopen(const char *filename, const char *mode);

  2. fclose:用于关闭文件。它的原型是int fclose(FILE *stream);

  3. fread:从文件中读取数据。它的原型是size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

  4. fwrite:将数据写入文件。它的原型是size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

  5. fgetc:从文件中获取一个字符。它的原型是int fgetc(FILE *stream);

  6. fputc:将一个字符写入文件。它的原型是int fputc(int c, FILE *stream);

  7. fgets:从文件中读取一行数据。它的原型是char *fgets(char *str, int n, FILE *stream);

  8. fputs:将一个字符串写入文件。它的原型是int fputs(const char *str, FILE *stream);

  9. feof:检查文件结束标志。它的原型是int feof(FILE *stream);

  10. fseek:设置文件位置指针。它的原型是int fseek(FILE *stream, long offset, int whence);

  11. ftell:获取文件位置指针的当前位置。它的原型是long ftell(FILE *stream);

  12. rewind:将文件位置指针重新设置为文件的开头。它的原型是void rewind(FILE *stream);

这些函数和操作在C程序中有效地读取和写入文件,以及在文件中移动和定位。

1.2 输入输出流

在C语言中,有三个默认打开的文件指针,分别是stdinstdoutstderr。它们都是指向FILE结构体类型的指针。

  1. stdin:这是标准输入流,通常关联着键盘输入。你可以使用scanf函数从stdin读取用户的输入。

  2. stdout:这是标准输出流,通常关联着屏幕或控制台。你可以使用printf函数将输出打印到stdout,然后显示在屏幕上。

  3. stderr:这是标准错误流,通常也关联着屏幕或控制台。不同于stdoutstderr被用于输出错误消息和警告。在一些情况下,你可能希望将错误信息输出到stderr,而不是stdout,以便能够更好地区分标准输出和错误输出。

这三个文件指针(stdinstdoutstderr)是在C程序开始执行时自动打开的,无需显式调用fopen函数来打开它们。它们提供了一种简单而有效的方式,使程序能够进行输入和输出操作,而无需过多关注底层文件操作。

例如,在C语言中,可以使用fprintf函数将输出写入到指定的文件指针,或者直接使用printf函数将输出写入到stdout,并且使用scanf函数从stdin读取输入数据。

#include <stdio.h>

int main() {
    int num;
    printf("Enter a number: "); // Output to stdout
    scanf("%d", &num); // Read input from stdin
    fprintf(stderr, "This is an error message!\n"); // Output an error message to stderr
    return 0;
}

这样,C语言提供了方便的方式来处理输入输出,而不必直接与底层文件操作打交道。

2、系统文件接口

除了使用C/C++接口来进行文件访问外,还可以使用系统接口或操作系统提供的文件访问方法。通常,系统接口是以系统调用的形式提供的,允许程序直接与操作系统进行交互。不同操作系统可能提供不同的系统接口,例如,Windows提供了一组系统调用,而Linux提供了另一组系统调用。

主要区别如下:

  1. 语言依赖性: C/C++接口是特定于C/C++语言的,这意味着它们只能在C/C++程序中使用。而系统接口是与操作系统相关的,所以可以在任何支持该操作系统系统调用的编程语言中使用,不仅限于C/C++。

  2. 抽象级别: C/C++接口提供了一组高级的抽象函数来操作文件,如fopenfwritefread等。这些函数隐藏了许多底层的细节,使文件操作更加简单。而系统接口通常提供了更底层的原始系统调用,如openwriteread等,需要程序员处理更多的细节,比如文件描述符、字节大小等。

  3. 可移植性: C/C++接口是C/C++标准的一部分,所以在大多数支持C/C++的平台上是可移植的。但是,不同操作系统可能提供不同的系统接口,因此使用系统接口可能会降低程序的可移植性。

  4. 功能和性能: C/C++接口提供了一些高级功能,如格式化输出和输入等,方便快捷,但在某些情况下可能会牺牲一些性能。而使用系统接口可以直接调用操作系统提供的原始功能,可能更加高效。

  5. 权限和控制: 使用C/C++接口时,文件的权限通常由程序自身的权限和运行环境决定。而使用系统接口时,可以更精确地控制文件的权限和访问级别,因为系统调用通常允许更细粒度的配置。

总体而言,C/C++接口是更高级、更方便、更易用的文件访问方式,适用于大多数简单的文件操作。而系统接口提供了更底层、更灵活的访问方式,适用于需要更精细控制、更高性能的场景,但可能会牺牲一些可移植性和易用性。在选择接口时,取决于项目的具体需求和考虑到可移植性、性能以及代码复杂性等因素。

2.1 接口介绍

Linux系统接口是Linux操作系统提供的一组系统调用,它允许程序与操作系统进行交互,执行各种底层操作。这些系统接口在Linux中被实现为系统调用(System Call),通过这些调用,用户空间程序可以请求操作系统执行特定的功能,例如文件访问、进程管理、网络通信等。

以下是一些常见的Linux系统接口:

  1. 文件操作: Linux提供了一系列系统调用来进行文件操作,如openreadwritecloselseek等。这些系统调用允许程序打开文件、读取数据、写入数据和关闭文件,同时支持文件定位。

  2. 进程管理: Linux系统接口允许程序创建新的进程,如forkexec系统调用,也可以进行进程等待和终止,如waitexit系统调用。

  3. 进程通信: Linux支持多种进程间通信(IPC)机制,包括管道(pipe)、命名管道(mkfifo)、共享内存(shmget)、消息队列(msgget)和信号量(semget)等。

  4. 网络通信: Linux提供了丰富的网络编程接口,如套接字(socket)编程,允许程序通过网络进行数据传输,包括TCP、UDP、IP等协议。

  5. 信号处理: Linux系统接口支持信号处理机制,允许程序捕捉和处理各种信号,如中断、错误等。

  6. 内存管理: Linux提供了系统调用来进行内存管理,如mallocfree等。

  7. 时间和定时器: Linux支持计时器和时间相关的系统调用,如gettimeofdaytimer_create等。

  8. 用户和组: Linux系统接口允许程序获取和管理用户和组的信息,如getuidgetgidgetpwuid等。

  9. 设备管理: Linux支持设备文件和设备管理系统调用,如ioctlmknod

  10. 文件系统操作: Linux系统接口允许程序对文件系统进行操作,如mkdirrmdirstatchmod等。

这只是Linux系统接口的一小部分,实际上Linux提供了更多的系统调用和接口,允许程序员编写功能强大且高效的应用程序。编程时,可以通过使用C或其他支持Linux系统调用的编程语言来访问这些接口,实现更加灵活和底层的系统功能。

2.2 接口用法

以下是一些重要的接口及其用法:

  1. 文件操作:

    • int open(const char *pathname, int flags, mode_t mode);:打开文件并返回一个文件描述符。
    • ssize_t read(int fd, void *buf, size_t count);:从文件描述符读取数据到缓冲区。
    • ssize_t write(int fd, const void *buf, size_t count);:将数据从缓冲区写入到文件描述符。
    • off_t lseek(int fd, off_t offset, int whence);:在文件中移动文件描述符的读/写位置。
  2. 进程管理:

    • pid_t fork(void);:创建一个新的进程,返回0在子进程,返回子进程ID在父进程。
    • pid_t getpid(void);:获取当前进程的ID。
    • int execve(const char *pathname, char *const argv[], char *const envp[]);:在当前进程中加载并执行一个新程序。
  3. 进程通信:

    • int pipe(int pipefd[2]);:创建一个管道,允许进程之间通过管道进行通信。
    • int shmget(key_t key, size_t size, int shmflg);:创建或获取一个共享内存区域。
    • int msgget(key_t key, int msgflg);:创建或获取一个消息队列。
  4. 网络通信:

    • int socket(int domain, int type, int protocol);:创建一个新的套接字。
    • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);:将套接字绑定到一个特定的地址和端口号。
    • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);:建立与远程主机的连接。
    • ssize_t send(int sockfd, const void *buf, size_t len, int flags);:发送数据到套接字。
    • ssize_t recv(int sockfd, void *buf, size_t len, int flags);:从套接字接收数据。
  5. 信号处理:

    • void (*signal(int signum, void (*handler)(int)))(int);:注册信号处理函数。
    • int kill(pid_t pid, int sig);:向指定进程发送信号。
  6. 内存管理:

    • void *malloc(size_t size);:分配指定大小的内存块。
    • void free(void *ptr);:释放之前分配的内存块。
  7. 设备管理:

    • int ioctl(int fd, unsigned long request, ...);:控制设备的特定操作。
  8. 文件系统操作:

    • int mkdir(const char *pathname, mode_t mode);:创建一个新的目录。
    • int rmdir(const char *pathname);:删除一个空目录。
    • int stat(const char *pathname, struct stat *buf);:获取文件或目录的状态信息。
    • int chmod(const char *pathname, mode_t mode);:更改文件或目录的权限。

以上是这些接口的简要介绍。实际使用时,还需要查阅相关文档和手册,以了解每个系统接口的更多细节和参数。这些接口为编写功能强大的Linux应用程序提供了基础。

3、文件描述符

3.1 意义

文件描述符(File Descriptor)是一个用来标识已经打开的文件的整数值。在Unix/Linux等操作系统中,一切都是文件,包括普通文件、设备文件、管道、套接字等,而文件描述符就是用来访问这些文件的一种机制。

文件描述符的作用是为了让进程能够对已经打开的文件进行读取、写入或其他操作。每当一个进程打开或创建一个文件,操作系统会为该文件分配一个唯一的文件描述符。文件描述符充当了进程与文件之间的桥梁,使进程可以通过文件描述符与文件进行交互,而无需关心文件在内核中的具体表示和管理方式。

在大多数Unix/Linux系统中,文件描述符的整数值从0开始递增。通常,前三个文件描述符被预留给标准输入(stdin,文件描述符为0)、标准输出(stdout,文件描述符为1)和标准错误(stderr,文件描述符为2)。这些文件描述符允许进程与用户交互,并将输出显示在终端上。

其他已经打开的文件将会被分配其他文件描述符值,这些值可能是3、4、5等等。文件描述符在进程的生命周期内是唯一的,因此可以通过文件描述符来识别和访问特定的文件。

文件描述符在系统调用中经常被使用,比如readwriteclose等函数都需要指定文件描述符来读取、写入或关闭文件。文件描述符的使用是Unix哲学的一部分,通过简单而一致的接口来处理文件和其他I/O资源,从而提供了强大和灵活的编程方式。

3.2 本质

在Linux进程中,默认情况下,会有三个预先打开的文件描述符,它们分别是标准输入(文件描述符 0)、标准输出(文件描述符 1)和标准错误(文件描述符 2)。这些文件描述符的默认用途如下:

  • 标准输入(文件描述符 0,通常为键盘): 用于接收进程的输入数据。当你使用类似scanf的函数从标准输入读取数据时,实际上是从键盘输入数据。

  • 标准输出(文件描述符 1,通常为显示器): 用于输出进程的普通输出。当你使用类似printf的函数输出数据时,实际上是输出到显示器上。

  • 标准错误(文件描述符 2,通常为显示器): 用于输出进程的错误信息。当程序出现错误时,你可以将错误信息输出到标准错误,而不是标准输出,以便将错误信息与正常输出区分开来。

这种约定有助于进程的交互和调试。例如,如果你希望用户输入某些数据,你可以使用标准输入;如果你想向用户显示一些信息,你可以使用标准输出;如果发生错误并需要输出错误信息,你可以使用标准错误。

当进程启动时,这三个文件描述符通常会自动与终端(键盘和显示器)相关联,但你也可以通过重定向(Redirection)的方式将它们与其他文件或设备关联,从而实现输入输出的重定向。

例如,你可以使用以下方式将标准输出和标准错误输出到同一个文件中:

./my_program > output.txt 2>&1

这会将程序my_program的标准输出和标准错误都重定向到名为output.txt的文件中。

在Linux中,每个进程都有一个指针files,指向一个files_struct结构体,该结构体中最重要的部分是一个指针数组,其中的每个元素都指向打开的文件。

实际上,Linux内核维护了一个称为"文件描述符表"(File Descriptor Table)的数据结构,这个表就是指针数组。当进程打开一个文件时(使用open系统调用或其他打开文件的方式),操作系统会分配一个可用的文件描述符,并将其分配给该打开的文件。而这个文件描述符就是文件描述符表中的索引,它使得进程可以通过简单的整数值来标识和访问对应的打开文件。

因为文件描述符是一个整数值,所以在Unix/Linux中,0、1、2这三个预先打开的文件描述符分别对应标准输入、标准输出和标准错误。从而,通过这三个默认的文件描述符,进程可以与键盘、显示器(终端)进行交互。

在Linux中,文件描述符的管理是由操作系统内核负责的。当进程打开或关闭文件时,内核会在文件描述符表中更新对应的项。这种机制使得进程能够轻松地管理打开的文件,并通过文件描述符进行文件的读取、写入和其他操作,而不必了解文件在内核中的具体表示方式。

总结起来,文件描述符是进程与打开的文件之间的桥梁,通过简单的整数值,进程可以定位到文件描述符表中对应的项,进而访问打开的文件。这种设计为Unix/Linux系统提供了一种简单而灵活的文件访问机制。

3.3 描述符分配

在Linux内核中,文件描述符的分配规则遵循如下原则:

  1. 最小可用文件描述符: 内核会在files_struct结构体中维护一个文件描述符表,这个表是一个指针数组。当进程打开一个文件时,内核会寻找在文件描述符表中当前没有被使用的最小的下标,作为新的文件描述符。

  2. 顺序分配: 文件描述符通常是按顺序分配的,从小到大。即第一个打开的文件会获得文件描述符 0,第二个打开的文件会获得文件描述符 1,以此类推。这样,文件描述符表中的下标和文件描述符的值是一一对应的。

  3. 最低未使用下标: 在遍历文件描述符表时,内核会查找数组中最小的未使用下标,将其分配给新的打开文件。这样做的目的是为了保证文件描述符的连续性,同时最小未使用下标可能有利于提高文件描述符的查找效率。

  4. 文件描述符上限: 文件描述符的分配是有上限的,这个上限通常由系统限制,通过ulimit命令可以查看当前用户的文件描述符上限。在一些配置中,文件描述符的上限可能会被限制为一定的值,比如1024或4096。

  5. 文件描述符的释放: 当进程关闭一个文件时,对应的文件描述符会被释放,重新变成可用状态。此时,其他打开文件或新的打开文件可以使用该文件描述符。

需要注意的是,对于新创建的进程,它会继承父进程的文件描述符表,包括已打开的文件和文件描述符状态。当然,子进程在创建后可以关闭不需要的文件描述符,并打开新的文件。

文件描述符的管理由操作系统内核负责,程序员在编写代码时通常无需直接操纵文件描述符表。通过使用标准C库提供的fopenclose等函数,以及相关的I/O函数,进程可以很方便地进行文件的读取和写入,而无需过多关注底层的文件描述符分配和管理。

3.4 重定向

重定向的本质是改变进程的标准输入、标准输出和标准错误的来源或目标。在Unix/Linux中,一切皆文件,包括键盘、显示器、设备和普通文件,而这些标准输入、标准输出和标准错误也都是特殊的文件描述符。

当一个进程运行时,它会默认从标准输入接收输入数据,并将输出和错误信息输出到标准输出和标准错误。这些标准输入、标准输出和标准错误通常是与终端(键盘和显示器)关联的。

重定向实际上就是将这些默认的文件描述符与其他文件或设备进行连接。通过重定向,我们可以改变进程的输入来源或输出目标,使得进程不再直接与终端交互,而与其他文件或设备交互。

具体来说:

  • 标准输入重定向就是将进程的输入从键盘改为从文件中读取数据,这样进程会从指定的文件读取输入数据。
  • 标准输出重定向就是将进程的输出从终端显示改为将输出写入到文件中,这样进程的输出将被保存到指定的文件中,而不再显示在终端上。
  • 标准错误重定向类似于标准输出重定向,但它是将进程的错误信息输出到指定的文件中,而不再显示在终端上。

通过重定向,我们可以将进程的输入、输出和错误信息与终端解耦,从而实现更灵活和方便的输入输出操作。这在编写脚本、处理文件和进行系统管理等场景中非常有用。在Unix/Linux中,重定向是一种强大且常用的特性,使得进程可以以更多样化的方式进行输入输出。

当我们使用重定向时,我们可以使用一些实际的例子来说明它的用法和效果。以下是一些使用重定向的典型示例:

  1. 输出重定向到文件:
    假设我们有一个简单的C程序,叫做hello.c,其内容如下:
#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

我们可以将该程序编译并运行,并将输出重定向到一个文件中,例如output.txt

gcc hello.c -o hello
./hello > output.txt

现在,output.txt文件中将包含程序的输出,而终端上将不会显示任何内容。

  1. 输入重定向来自文件:
    假设我们有一个名为input.txt的文件,其中包含了一些输入数据,例如:
John
Doe

我们可以编写一个简单的C程序,从标准输入读取数据,并将输入重定向到input.txt文件:

#include <stdio.h>

int main() {
    char name[100];
    printf("Enter your name: ");
    scanf("%s", name);
    printf("Hello, %s!\n", name);
    return 0;
}

然后,我们可以运行程序并使用输入重定向来自input.txt文件:

./hello < input.txt

这样,程序将从input.txt文件中读取输入数据,并输出相应的问候消息。

  1. 合并标准输出和标准错误:
    假设我们有一个名为test_program的可执行文件,该程序可能会在标准输出和标准错误输出一些消息。我们可以将这两种输出合并到同一个文件中,以便方便查看:
./test_program > output.txt 2>&1

在这个例子中,2>&1操作符将标准错误重定向到标准输出,从而将所有输出都写入到output.txt文件中。

重定向是一种非常实用的技术,在Shell脚本、系统管理、日志记录等方面都得到广泛的应用。通过将进程的输入输出与终端解耦,我们可以更加灵活地处理数据和输出信息。

4 FILE

在Unix/Linux系统中,C标准库(libc)为文件操作提供了高级抽象接口,比如fopenfwritefread等函数。这些函数封装了底层的系统调用,如openwriteread等,使得文件的读写操作更加简单和易用。

在C标准库中,FILE结构体用于表示文件流,它在内部确实包含了一个文件描述符(int fd)以及其他用于文件缓冲和管理的信息。文件流提供了一个高级的抽象层,让程序员无需直接处理文件描述符,而可以使用更方便的API进行文件的读写。

当使用fopen函数打开一个文件时,C库会调用底层的open系统调用获取一个文件描述符,然后将其存储在FILE结构体中。之后,通过FILE结构体的操作,如fwritefread等函数,可以对文件进行读写,而无需直接操纵文件描述符。

这种高级抽象的设计使得文件的访问更加易用和可移植。程序员可以使用C标准库提供的函数来进行文件操作,而无需担心底层的系统调用细节。同时,C标准库的文件流也允许对文件的缓冲和其他优化进行管理,从而提高文件的读写效率。

总结起来,C标准库的FILE结构体内部封装了文件描述符,并提供了高级的文件访问接口,使得文件的读写操作更加方便和可控。这种设计是为了提供更好的抽象和易用性,并同时保留底层系统调用的灵活性。

4.1 缓冲区

缓冲区和FILE之间有密切的关系,特别是在C标准库中进行文件读写时。每个FILE结构体都包含一个缓冲区,用于在用户空间中临时存储待写入文件或待读取自文件的数据。这个缓冲区是用户级的,由C标准库提供,并用于提高文件的读写效率。

当我们使用C标准库的函数如printffwritefread等进行文件读写时,这些函数会使用FILE结构体内部的缓冲区来存储数据,而不是直接将数据写入磁盘或从磁盘读取数据。缓冲区的存在允许这些库函数将多个小的读写操作合并为更大的批量操作,从而提高性能。

具体来说,对于输出操作(写入文件),C标准库会在FILE结构体中设置一个输出缓冲区。当我们调用printf等函数输出数据时,数据会首先被存储在这个缓冲区中,直到缓冲区满了或者手动调用fflush函数,缓冲区中的数据才会被一次性写入磁盘。这样可以减少写入磁盘的次数,从而提高效率。

对于输入操作(从文件读取数据),C标准库也会在FILE结构体中设置一个输入缓冲区。当我们调用fread等函数读取数据时,这些函数会先检查缓冲区中是否有足够的数据可以提供给读取操作。如果缓冲区中的数据不足,C标准库会执行一个系统调用来从磁盘中读取更多的数据到缓冲区中。这样可以减少读取磁盘的次数,从而提高效率。

总的来说,缓冲区是C标准库为文件读写操作提供的一个缓冲机制,使得读写操作可以更加高效地进行。这种缓冲机制是用户级的,与底层操作系统的缓冲机制相互独立。通过合理使用缓冲区,我们可以在应用程序中获得更好的性能和灵活性。

在应用程序中,缓冲区的存在会对文件写入操作产生一些特定的现象和影响:

  1. 高效的批量写入: 缓冲区的存在允许将多个小的写入操作合并为一个更大的批量写入操作。这种批量写入可以显著提高文件写入的效率,减少与内核的交互次数,从而减少了系统调用的开销。

  2. 延迟写入: 缓冲区中的数据并不会立即写入磁盘,而是在缓冲区被填满、或者手动调用fflush函数、或者文件关闭时才会触发实际的写入操作。这样可以延迟实际的写入操作,将多次小写入合并为一次较大的写入,进一步提高性能。

  3. 程序崩溃时数据丢失: 如果缓冲区中的数据还未写入到磁盘,而程序崩溃了,那么这部分数据将会丢失。因为缓冲区中的数据还未持久化到磁盘上,只有在缓冲区的数据写入到磁盘后,数据才是安全的。

  4. 文件内容不即时可见: 当数据写入缓冲区后,虽然数据已经被写入到了用户空间的缓冲区,但磁盘上的文件内容并不会立即更新。只有在缓冲区的数据写入磁盘后,其他程序或用户才能看到文件的最新内容。

  5. 手动刷新缓冲区: 可以通过调用fflush函数来手动刷新缓冲区,强制将缓冲区中的数据写入到磁盘。这在需要确保数据及时持久化的场景中非常有用。

对于不同的应用场景,合理使用缓冲区可以带来显著的性能提升,但也需要注意在数据持久性和实时性方面的权衡。例如,在实时数据采集和日志记录等需要确保数据及时持久化的场景中,可能需要频繁地调用fflush函数来手动刷新缓冲区,以确保数据不会因为延迟写入而丢失。而在一些性能要求较高的场景,可以适当增大缓冲区,利用缓冲区的批量写入特性来提高性能。

5、文件系统

使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。ls -l读取存储在磁盘上的文件信息,然后显示出来:

  • 模式
  • 硬链接数
  • 文件所有者
  • 大小
  • 最后修改时间
  • 文件名

5.1 基本思想

Linux文件系统的设计思想可以概括为以下几个主要方面:

  1. 一切皆文件: 在Linux中,一切设备、资源和数据都被视为文件。这意味着磁盘、键盘、显示器、网络设备、进程和内核信息等都被抽象为文件的形式,统一了对它们的访问和管理方式。这种思想使得文件系统提供了一种统一的接口,使得用户和应用程序能够以类似的方式来访问和处理不同类型的资源。

  2. 层次化目录结构: Linux文件系统采用层次化的目录结构,类似于树形结构。根目录是所有目录的起始点,每个目录下可以包含子目录和文件。这种层次化结构使得文件系统的组织更加有序,用户和程序可以通过简单的路径来访问和定位文件。

  3. 虚拟文件系统: Linux引入了虚拟文件系统(Virtual File System,VFS)的概念,它提供了一个统一的接口层,将不同类型的文件系统(如ext2、ext3、NTFS等)抽象为相同的接口。VFS隐藏了底层文件系统的细节,使得用户和应用程序可以使用相同的系统调用来访问不同类型的文件系统,提高了文件系统的可移植性和扩展性。

  4. Inode和文件名分离: Linux的文件系统使用Inode和文件名分离的方式来管理文件。每个文件都有一个唯一的Inode,它包含了文件的元数据信息,如权限、大小、链接数等。文件名与Inode相互关联,而不是直接与文件数据关联,这样可以支持硬链接和软链接等特性,同时提高了文件系统的效率。

  5. 读写权限控制: Linux文件系统使用权限位来控制文件的读、写和执行权限。每个文件和目录都有一个属主和一个属组,通过设置不同的权限位,可以精细地控制对文件的访问和操作权限,保护文件的安全性和隐私。

这些设计思想使得Linux文件系统成为一个功能强大、灵活且高效的文件管理系统。它提供了统一的接口、层次化的目录结构、虚拟文件系统的抽象、分离的Inode和文件名、以及细粒度的权限控制,为用户和应用程序提供了便捷的文件访问和管理方式。

5.2 inode

Inode(Index Node)是文件系统中一个重要的概念,用于存储文件的元数据信息。在Linux和其他类Unix系统中,每个文件都对应一个唯一的inode。Inode中包含了文件的诸多属性,例如文件的大小、所属用户和组、访问权限、创建时间、修改时间、链接数以及数据块的指针等信息。

在文件系统中,文件名与inode是一一对应的关系。当我们在文件系统中创建一个文件时,系统会为该文件分配一个inode,并将文件名与inode关联。文件系统通过inode来标识和管理文件,而文件名只是inode的一个别名。

当我们访问一个文件时,文件系统首先通过文件名查找到对应的inode,然后根据inode中的信息来访问文件的数据和属性。这种通过inode来访问文件的方式可以提高文件系统的性能和效率,因为文件名查找只需要在目录中搜索,而inode包含了文件的所有属性,避免了重复的属性查询。

一些与inode相关的重要概念和特点包括:

  1. 硬链接(Hard Link): 在一个文件系统中,可以有多个文件名指向同一个inode,这些文件名之间的关联称为硬链接。硬链接允许一个inode对应多个文件名,删除其中一个文件名并不会删除文件数据,只有当所有关联的文件名都被删除后,inode才会被释放。

  2. 软链接(Symbolic Link): 也称为符号链接,软链接是一种特殊的文件类型,它包含了指向另一个文件的路径。软链接类似于Windows系统中的快捷方式,它是一个新的inode,拥有自己的文件属性,而不是像硬链接一样与目标文件共享inode。

  3. 文件系统的inode数量限制: 文件系统的inode数量是在文件系统格式化时确定的,它决定了文件系统可以创建的文件和目录数量的上限。当文件系统的inode数量用尽时,无法再创建新的文件,但可以继续在现有文件上创建硬链接。

总结:Inode是文件系统中的重要概念,它用于存储文件的元数据信息。文件系统通过inode来标识和管理文件,而文件名只是inode的一个别名。通过使用inode,文件系统可以高效地访问文件数据和属性,支持硬链接和软链接等特性,从而提供了灵活和高效的文件管理机制。

6、动态静态库

动态库和静态库是在软件开发中常用的两种库文件,用于存放可重用的代码和函数。它们有不同的编译和链接方式,以及在程序运行时的行为。

  1. 静态库(Static Library):
    静态库是在编译时将库代码的副本嵌入到目标程序中的库文件。在编译过程中,目标程序会直接把静态库中的函数和代码链接到最终的可执行文件中。这意味着目标程序在运行时不再依赖于静态库,而是包含了库的副本。静态库的文件扩展名通常为.a(在Windows环境下为.lib)。

优点:

  • 简单,不依赖其他文件,一次编译即可生成可执行文件,移植性好。
  • 可以避免与其他版本的库发生冲突。

缺点:

  • 每个使用该库的程序都会包含库的副本,导致可执行文件较大。
  • 更新库时需要重新编译程序,不利于库的版本升级。
  1. 动态库(Dynamic Library):
    动态库是在运行时由操作系统加载的库文件。目标程序在编译时并不包含动态库的副本,而是在运行时通过动态链接器(如ld.soDLL)在内存中加载库的代码。动态库的文件扩展名通常为.so(在Windows环境下为.dll)。

优点:

  • 可执行文件较小,因为不包含库的副本,而是在运行时加载。
  • 动态库可共享,多个程序可以共用同一个库,节省内存。

缺点:

  • 需要动态库的支持,因此在其他计算机上运行程序时需要确保动态库的存在。
  • 对库版本有要求,如果运行时的动态库版本不兼容,可能会导致程序崩溃或错误。

总结:静态库在编译时将库代码嵌入到目标程序中,不依赖于其他文件;动态库在运行时加载,需要动态库的支持。选择使用静态库还是动态库取决于项目需求和开发的具体情况。在实际开发中,通常根据项目的要求和可执行文件大小等方面的考虑来选择使用哪种库。

6.1 库搜索路径

在Linux系统中,链接器(通常是GNU ld)在搜索库文件时,会按照一定的搜索路径顺序进行查找。这些搜索路径包括:

  1. -L指定的目录: 在链接时,可以使用-L选项指定特定的库文件搜索目录。例如,-L/path/to/libs会告诉链接器在/path/to/libs目录下搜索库文件。

  2. 环境变量LIBRARY_PATH: 环境变量LIBRARY_PATH可以用来指定额外的库文件搜索路径。如果设置了LIBRARY_PATH,链接器将会在该路径下搜索库文件。

  3. 系统指定的目录: 系统中预定义的一些默认库文件搜索路径。这些路径通常由系统维护,例如在/lib/usr/lib等目录下搜索库文件。

  4. /usr/lib: 该目录是一个常见的系统库目录,用于存放系统的标准库文件。

  5. /usr/local/lib: 该目录通常用于存放本地安装的自定义库文件。

链接器会按照上述顺序依次在这些路径下搜索所需的库文件。当找到需要的库文件后,链接器会将它们与程序一起链接,生成最终的可执行文件。

这种搜索路径的设计允许开发人员在不同的位置安装库文件,并让链接器能够找到并正确链接这些库,提高了库的可用性和灵活性。同时,通过环境变量和特定的链接选项,开发人员可以在需要时自定义库文件的搜索路径,确保链接器可以找到正确的库文件。

6.2 生成动态库

生成动态库可以使用编程语言的编译器和链接器工具。下面以C语言为例,介绍生成动态库的步骤:

  1. 编写源代码: 首先,编写包含要导出函数或符号的C语言源代码文件。例如,我们创建一个名为mylib.c的文件,其中包含一个简单的函数。
// mylib.c
#include <stdio.h>

void say_hello() {
    printf("Hello, World!\n");
}
  1. 编译源代码: 使用C语言编译器(如gcc)编译源代码生成目标文件。在这里,我们将生成一个名为mylib.o的目标文件。
gcc -c -fPIC mylib.c

选项说明:

  • -c:表示仅编译源代码,不进行链接,生成目标文件。
  • -fPIC:生成位置无关代码(Position-Independent Code),这是生成动态库所必需的。
  1. 生成动态库: 使用C语言编译器和链接器将目标文件生成动态库。在Linux系统上,动态库的文件名通常以.so结尾,并使用-shared选项来生成动态库。
gcc -shared -o libmylib.so mylib.o

选项说明:

  • -shared:表示生成一个共享对象,即动态库。

现在,你已经成功生成了名为libmylib.so的动态库。

  1. 使用动态库: 现在你可以在其他程序中使用这个动态库。假设我们有一个名为main.c的程序,想要调用动态库中的函数。
// main.c
#include <stdio.h>

// 声明动态库中的函数
extern void say_hello();

int main() {
    // 调用动态库中的函数
    say_hello();
    return 0;
}

接下来,我们需要使用动态库链接我们的程序:

gcc -o my_program main.c -L. -lmylib

选项说明:

  • -L.:告诉链接器在当前目录中搜索库文件。
  • -lmylib:告诉链接器链接名为libmylib.so的动态库。

现在,我们的程序my_program可以成功调用动态库中的函数,并输出"Hello, World!"。

注意:在使用动态库时,确保动态库文件(例如libmylib.so)在系统的标准动态库搜索路径下或者设置LD_LIBRARY_PATH环境变量指向动态库所在的路径,以便程序能够找到并加载动态库。

6.3 使用动态库

使用动态库涉及编写程序、生成动态库文件和链接程序这几个步骤。下面以C语言为例,演示如何使用动态库:

  1. 编写动态库的源代码: 首先,我们编写包含要导出函数的C语言源代码文件。假设我们有一个简单的数学库,我们在一个名为mathlib.c的文件中实现两个函数:加法和乘法。
// mathlib.c
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}
  1. 编译动态库的源代码: 使用C语言编译器编译源代码生成目标文件,并将目标文件生成动态库。在Linux系统上,使用-shared选项生成动态库,并使用-fPIC选项生成位置无关代码。
gcc -c -fPIC mathlib.c
gcc -shared -o libmathlib.so mathlib.o
  1. 编写程序并链接动态库: 编写一个调用动态库函数的程序,并使用动态库链接器将程序链接到动态库。假设我们有一个名为main.c的程序,调用动态库中的加法函数和乘法函数。
// main.c
#include <stdio.h>

// 声明动态库中的函数
extern int add(int a, int b);
extern int multiply(int a, int b);

int main() {
    int result_add = add(5, 3);
    int result_multiply = multiply(5, 3);

    printf("Addition result: %d\n", result_add);
    printf("Multiplication result: %d\n", result_multiply);

    return 0;
}
  1. 链接程序并运行: 使用C语言编译器将程序链接到动态库,并生成可执行文件。然后运行可执行文件来调用动态库中的函数。
gcc -o my_program main.c -L. -lmathlib
./my_program

选项说明:

  • -L.:告诉链接器在当前目录中搜索库文件。
  • -lmathlib:告诉链接器链接名为libmathlib.so的动态库。

运行上述命令后,你应该会看到输出结果:

Addition result: 8
Multiplication result: 15

这表明程序成功调用了动态库中的函数,并得到了正确的结果。

请注意,在使用动态库时,确保动态库文件(例如libmathlib.so)在系统的标准动态库搜索路径下或者设置LD_LIBRARY_PATH环境变量指向动态库所在的路径,以便程序能够找到并加载动态库。

6.4 运行动态库

在Linux系统中运行动态库涉及两个步骤:

  1. 编写程序并链接动态库: 首先,需要编写一个程序,并将其链接到动态库。假设已经有一个名为main.c的程序,调用了动态库中的函数。
// main.c
#include <stdio.h>

// 声明动态库中的函数
extern void say_hello();

int main() {
    // 调用动态库中的函数
    say_hello();
    return 0;
}
  1. 链接程序并运行: 使用C语言编译器将程序链接到动态库,并生成可执行文件。然后运行可执行文件来调用动态库中的函数。
gcc -o my_program main.c -L. -lmylib
./my_program

选项说明:

  • -L.:告诉链接器在当前目录中搜索库文件。
  • -lmylib:告诉链接器链接名为libmylib.so的动态库。

在运行./my_program之前,确保动态库文件(例如libmylib.so)在系统的标准动态库搜索路径下或者设置LD_LIBRARY_PATH环境变量指向动态库所在的路径,以便程序能够找到并加载动态库。

当运行./my_program时,程序会调用动态库中的函数,并输出相应的结果。

请注意,动态库在运行时是通过动态链接器加载的,因此在运行程序时,确保动态库文件的路径是可被动态链接器找到的。如果动态库文件的路径不在动态链接器的搜索路径中,可以通过LD_LIBRARY_PATH环境变量来指定动态库的路径。

另外,动态库的加载是在运行时进行的,这使得在动态库更新后,不需要重新编译程序即可使用新的库文件。这为库的升级和维护带来了便利。

6.5 使用外部库

使用外部库的步骤主要包括获取库文件、编写程序并链接库,然后编译并运行程序。下面以C语言为例,演示如何使用外部库:

  1. 获取库文件: 首先,你需要获取所需的外部库文件。通常外部库提供者会提供预编译的库文件(.a.so),以及相应的头文件(.h)供你使用。

  2. 编写程序并链接库: 然后,你需要编写一个程序,并将其链接到外部库。假设你已经有一个名为main.c的程序,调用了外部库中的函数。

// main.c
#include <stdio.h>
#include "external_lib.h"  // 外部库的头文件

int main() {
    // 调用外部库中的函数
    int result = external_function(5, 3);
    printf("Result: %d\n", result);
    return 0;
}
  1. 编译程序并链接外部库: 使用C语言编译器将程序链接到外部库,并生成可执行文件。
gcc -o my_program main.c -L/path/to/library -lexternallib

选项说明:

  • -o my_program:指定生成的可执行文件的名称为my_program
  • main.c:源代码文件名。
  • -L/path/to/library:告诉链接器在指定目录/path/to/library中搜索库文件。
  • -lexternallib:告诉链接器链接名为libexternallib.so(或libexternallib.a)的外部库。注意,这里只需提供库名,链接器会自动根据操作系统查找相应的动态库或静态库。
  1. 运行程序: 在完成编译后,你可以运行生成的可执行文件。
./my_program

程序会调用外部库中的函数,并输出结果。

请注意,你需要确保外部库文件和头文件在编译时能够被找到。可以通过-I选项指定头文件所在的路径,通过-L选项指定库文件所在的路径。如果使用了动态库,还需要确保动态库文件的路径在系统的动态库搜索路径中,或者设置LD_LIBRARY_PATH环境变量来指定动态库的路径。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值