文章目录
- C语言文件操作
- 系统文件I/O
- 接口介绍
- open函数返回值
- 文件描述符fd
- 0 & 1 & 2
- 文件描述符的分配规则
- 重定向
- 使用 dup2 系统调用
- FILE
- 理解文件系统
- 理解硬链接
- 软链接
- acm
- 动态库和静态库
- 静态库与动态库
- 生成静态库
- 生成动态库:
C语言文件操作
先来段代码回顾C文件接口
hello.c写读文件:
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp)
{
printf("fopen error!\n");
}
const char *msg = "hello bit!\n";
int count = 5;
while(count--)
{
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
FILE *rfp = fopen("myfile", "r");
if(!rfp)
{
printf("fopen error!\n");
}
char buf[1024];
const char *msg = "hello bit!\n";
while(1)
{
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), rfp);
if(s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}
输出信息到显示器,你有哪些方法:
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
stdin & stdout & stderr:
- C默认会打开三个输入输出流,分别是stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型的文件指针。
总结:
打开文件的方式:
如上,是我们之前学的文件相关操作。还有 fseek ftell rewind 的函数,在C部分已经有所涉猎。
系统文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:
hello.c 写文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0)
{
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello bit!\n";
int len = strlen(msg);
while(count--)
{
write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址,
//len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
}
close(fd);
return 0;
}
hello.c读文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
const char *msg = "hello bit!\n";
char buf[1024];
while(1)
{
ssize_t s = read(fd, buf, strlen(msg));//类比write
if(s > 0)
{
printf("%s", buf);
}
else
{
break;
}
}
close(fd);
return 0;
}
接口介绍
在Linux中,open()
是一个系统调用函数,用于打开或创建文件。它的原型如下:
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open()
函数接受两个参数,第一个参数是要打开或创建的文件路径名(字符串类型),第二个参数是标志位,指定了打开文件的方式和属性。
以下是常见的 flags
标志位选项:
O_RDONLY
:只读模式。O_WRONLY
:只写模式。O_RDWR
:读写模式。O_CREAT
:如果文件不存在,则创建新文件。O_TRUNC
:如果文件存在并且以可写方式打开,则将其截断为空文件。O_APPEND
:以追加方式打开文件,在已有内容末尾添加新数据。O_EXCL
:与O_CREAT
一起使用时,如果文件已经存在,则失败返回。O_NONBLOCK
:非阻塞模式打开文件。
除了上述标志位之外,还可以通过按位或运算符组合多个标志位来实现更复杂的操作。
如果使用了 O_CREAT
标志位,那么需要提供一个额外的参数 mode
来设置文件权限。mode
参数是一个无符号整数,表示文件的访问权限。常见的权限选项包括 S_IRUSR
、S_IWUSR
、S_IRGRP
、S_IWGRP
、S_IROTH
和 S_IWOTH
,分别表示用户、组和其他人的读写权限。
open()
函数返回一个整数值,即文件描述符(file descriptor)。如果打开或创建文件成功,则返回非负整数作为文件描述符
;否则,返回-1
表示出错。
write的使用
在Linux中,write()
是一个系统调用函数,用于将数据写入文件描述符(file descriptor)。它的原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write()
函数接受三个参数:文件描述符 fd
、要写入的数据缓冲区地址 buf
和要写入的字节数 count
。
以下是 write()
函数的使用方式:
#include <unistd.h>
int fd = open("filename.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd != -1) {
const char* buffer = "Hello, World!";
ssize_t bytes_written = write(fd, buffer, strlen(buffer));
if (bytes_written == -1) {
// 写入出错
} else {
// 成功写入指定字节数
}
close(fd);
} else {
// 文件打开或创建失败
}
上述示例代码首先使用 open()
函数打开或创建了一个文件,并获取到文件描述符 fd
。然后,定义一个字符串缓冲区 buffer
存储要写入的数据。接着,通过调用 write()
函数将缓冲区中的数据写入文件。strlen(buffer)
表示写入的字节数为字符串长度。write()
函数返回实际写入的字节数,如果返回值为-1,则表示写入出错。
需要注意的是,在使用完文件后,应使用 close()
函数关闭文件描述符以释放相关资源。
write()
函数可以用于向文件、终端、套接字等支持写入操作的设备进行数据写入。它是一个底层的系统调用函数,适用于对文件和设备进行较低级别的操作。
值得一提的是,在C++中,您也可以使用 <fstream>
头文件提供的文件流(std::ofstream
)来进行更方便和类型安全的文件写入操作。
read的使用方法
在Linux中,read()
是一个系统调用函数,用于从文件描述符(file descriptor)读取数据。它的原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read()
函数接受三个参数:文件描述符 fd
、存储读取数据的缓冲区地址 buf
和要读取的最大字节数 count
。
以下是 read()
函数的使用方式:
#include <unistd.h>
int fd = open("filename.txt", O_RDONLY);
if (fd != -1) {
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
// 读取出错
} else if (bytes_read == 0) {
// 文件已经到达末尾
} else {
// 成功读取指定字节数
// 可以对读取的数据进行处理
}
close(fd);
} else {
// 文件打开失败
}
上述示例代码首先使用 open()
函数以只读模式打开文件,并获取到文件描述符 fd
。然后,定义一个字符数组缓冲区 buffer
来存储读取的数据。接着,通过调用 read()
函数将文件中的数据读取到缓冲区中。sizeof(buffer)
表示最多读取的字节数为缓冲区大小。read()
函数返回实际读取的字节数,如果返回值为-1,则表示读取出错。如果返回值为0,表示文件已经到达末尾。
需要注意的是,在使用完文件后,应使用 close()
函数关闭文件描述符以释放相关资源。
read()
函数可以用于从文件、终端、套接字等支持读取操作的设备中获取数据。它是一个底层的系统调用函数,适用于对文件和设备进行较低级别的操作。
close的用法:
在Linux中,close()
函数用于关闭打开的文件描述符(file descriptor)。它的原型如下:
#include <unistd.h>
int close(int fd);
close()
函数接受一个整数参数 fd
,表示要关闭的文件描述符。
以下是 close()
函数的使用方法:
#include <unistd.h>
int fd = open("filename.txt", O_RDONLY);
if (fd != -1) {
// 对文件进行读取或写入操作
int result = close(fd);
if (result == -1) {
// 关闭文件失败
}
} else {
// 文件打开失败
}
上述示例代码首先使用 open()
函数以只读模式打开文件,并获取到文件描述符 fd
。然后,在执行其他对文件的读取或写入操作后,调用 close()
函数来关闭文件描述符。如果 close()
函数返回值为-1,则表示关闭文件失败。
需要注意的是,close()
函数会释放与文件描述符相关联的资源,包括操作系统内核维护的文件表项等。因此,在不再需要使用文件时,应该及时关闭文件描述符,以避免资源泄漏和浪费。
请注意,当进程终止时,所有打开的文件描述符都会自动关闭,所以通常情况下并不需要显式地调用 close()
函数。但是,在长时间运行的程序中,特别是涉及大量文件操作的程序中,及时关闭不再需要的文件描述符是一个良好的编程习惯。
lseek的使用方法:
在Linux中,lseek()
函数用于设置文件偏移量(file offset),它可以改变对文件的读取或写入位置。lseek()
函数的原型如下:
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
/*`off_t` 是一个数据类型,用于表示文件偏移量。它是在 `<unistd.h>` 头文件中定义的,
通常被定义为 `long int` 类型。`off_t` 的目的是提供足够大的整数类型,以便能够表示大
文件的偏移量。因为现代操作系统支持处理非常大的文件,所以需要使用一个能够容纳大文件大小
的数据类型来表示偏移量。在不同的系统上,`off_t` 可能会有不同的实现方式。例如,在32位
系统上,`off_t` 可能是一个32位的有符号整数(`int` 或 `long int`),而在64位系统上,
`off_t` 通常是一个64位的有符号整数(`long int` 或 `long long int`)。
因此,具体的 `off_t` 实际类型可能会根据编译器和操作系统的不同而有所变化。但一般来说,
它应该是一个能够表示大文件偏移量的整数类型。*/
lseek()
函数接受三个参数:文件描述符 fd
、偏移量 offset
和起始位置 whence
。
fd
是要进行操作的文件描述符。offset
是一个整数值,表示要相对于起始位置移动的字节数。正值将向后移动,负值将向前移动。whence
指定了起始位置,有以下几种选项:SEEK_SET
:从文件开头开始计算偏移量。SEEK_CUR
:从当前位置开始计算偏移量。SEEK_END
:从文件末尾开始计算偏移量。
以下是 lseek()
函数的使用方法示例:
#include <unistd.h>
#include <fcntl.h>
int fd = open("filename.txt", O_RDONLY);
if (fd != -1) {
off_t new_offset = lseek(fd, 0, SEEK_END);
if (new_offset == -1) {
// 获取文件偏移量失败
} else {
// 成功获取文件偏移量
// 可以根据需要进行读取或写入操作
}
close(fd);
} else {
// 文件打开失败
}
上述示例代码首先使用 open()
函数以只读模式打开文件,并获取到文件描述符 fd
。然后,调用 lseek()
函数将偏移量设置为相对于文件末尾的位置(通过 SEEK_END
参数)。如果 lseek()
函数返回值为-1,则表示设置文件偏移量失败。否则,可以根据需要进行读取或写入操作。
请注意,lseek()
函数常用于随机访问文件,允许在文件中任意位置进行读取和写入操作。它适用于支持定位操作的设备,如磁盘文件。
open函数返回值
在认识返回值之前,先来认识一下两个概念: 系统调用
和 库函数
当编写程序时,我们可以使用系统调用和库函数来访问操作系统提供的功能。这两个概念有一些区别:
系统调用(System Call):
系统调用是应用程序与操作系统之间进行交互的接口。它允许应用程序请求操作系统执行某些特权操作或获取底层资源。系统调用提供了对操作系统核心功能的直接访问。
在系统调用中,应用程序通过特定的指令(通常是软件中断)将控制权转移给操作系统内核,并传递参数以指示所需的操作。操作系统内核处理该请求并返回结果给应用程序。
系统调用涉及低级别的操作,例如文件管理、进程管理、网络通信等。由于系统调用需要切换到内核模式,因此执行系统调用可能比执行用户空间代码更耗时。一般而言,系统调用提供了较为底层的操作接口。
库函数(Library Function):
库函数是预先编写好的可重用代码块,封装了常见的操作和算法。它们位于动态链接库(shared library)或静态库(static library)中,供应用程序调用。
库函数通常提供高级别的抽象和简化的接口,使得开发人员能够更方便地使用各种功能。库函数隐藏了底层实现细节,提供了更高层次的抽象,使得开发人员可以更专注于应用程序逻辑。
库函数可以涉及各种领域,如字符串处理、数学运算、图形界面等。它们是在用户空间中执行的,无需切换到内核模式。由于库函数通常封装了一些常见操作,因此其执行速度可能比系统调用更快。
总结来说,系统调用是应用程序与操作系统之间进行交互的接口,提供对底层资源和功能的直接访问;而库函数是预先编写好的可重用代码块,提供了高级别的抽象和简化的接口,使得开发人员能够更方便地使用各种功能。
需要注意的是,库函数有时候也会调用底层的系统调用来完成特定任务。这样做的目的是利用系统调用提供的底层功能,并通过库函数的封装使其更易于使用。
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口 回忆一下我们讲操作系统概念时,
画的一张图
系统调用接口和库函数的关系,一目了然。
文件描述符fd
文件描述符(File Descriptor, 简称FD)是一个在Unix、Linux以及其他类Unix操作系统中广泛使用的概念。它是一个非常小的非负整数,由操作系统用来唯一标识一个打开的文件。当程序打开一个现有文件或创建一个新文件时,操作系统会创建一个文件描述符来代表该文件,并将其返回给程序。
文件描述符主要用于文件I/O操作,例如读取、写入、关闭文件等。这是一种底层的抽象,使得操作系统可以通过统一的接口操作各种类型的I/O资源,如普通文件、目录、套接字(socket)、管道(pipe)等。
在标准POSIX定义中,标准输入(stdin)、标准输出(stdout)和标准错误(stderr)分别有三个预定义的文件描述符:
- 0:标准输入
- 1:标准输出
- 2:标准错误
程序可以使用这些文件描述符进行基本的输入输出操作。例如,在C语言中,可以使用标准库函数如read
和write
来通过文件描述符进行读写操作。
以下是一些用于管理文件描述符的常用系统调用:
open
:打开或创建一个文件,并返回一个新的文件描述符。close
:关闭一个文件描述符,释放它所占用的资源。read
:从一个文件描述符指向的文件中读取数据。write
:向一个文件描述符指向的文件写入数据。dup
和dup2
:复制一个文件描述符,可以用于重定向输入输出。
在某些情况下,如并发服务器的设计中,文件描述符的管理显得尤为重要,因为需要高效地处理大量打开的文件。由于文件描述符是有限的资源,不正确的管理可能导致资源泄露问题,比如“文件描述符耗尽”。因此,在编写涉及文件操作的程序时,确保在不再需要文件描述符时关闭它们是很重要的。
0 & 1 & 2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
char buff[1024];
ssize_t s = read(0,buff,sizeof(buff));
if(s>0)
{
buff[s]=0;
write(1,buff,strlen(buff));
write(2,buff,strlen(buff));
}
return 0;
}
运行结果为:输入一行字,会在显示器上全打印出来。
底层内核实现图如下:
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来
描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进
程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标
。所以,只要拿着文件描述符,就可以找到对应的文件。
文件描述符的分配规则
直接看代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd =open("myfile.txt",O_RDONLY|O_CREAT);
if(fd<0)
{
perror("open fail");
return 1;
}
printf("fd: %d\n",fd);
close(fd);
return 0;
}
输出发现是 fd: 3
此时关闭0或者2,再看:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile.txt", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符。
重定向
那如果关闭1呢?看代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
close(1);
int fd = open("myfile.txt",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
perror("open fail");
return 1;
}
printf("fd:%d\n",fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出
重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢?见下图
使用 dup2 系统调用
dup2
是 UNIX 和类 UNIX 系统(如 Linux)中的一个系统调用,它用于复制文件描述符。给定两个文件描述符,dup2
会使第二个文件描述符成为第一个文件描述符的副本,如果必要的话,会关闭第二个文件描述符。这个操作常常用于重定向标准输入、输出和错误流。
dup2
函数的原型定义在 <unistd.h>
头文件中,其原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
参数:
oldfd
是已打开的文件描述符的副本。newfd
是你想要oldfd
复制到的文件描述符编号。
如果 newfd
已经打开,dup2
会先关闭它,然后再复制 oldfd
。如果 oldfd
是无效的文件描述符,dup2
不会关闭 newfd
,并且会返回错误。
返回值:
- 成功时,返回新的文件描述符(即
newfd
)。 - 如果出现错误,返回
-1
并设置errno
来表示错误类型。
使用示例:
假设你想要将程序的标准输出重定向到一个文件,你可以使用 open
系统调用打开(或创建)一个文件,获取其文件描述符,然后使用 dup2
将标准输出复制到这个文件描述符上。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// 打开一个文件用于写入。如果不存在就创建它,权限设置为 0644
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 复制文件描述符 fd 到标准输出(stdout)
if (dup2(fd, STDOUT_FILENO) == -1) {
perror("dup2");
exit(EXIT_FAILURE);
}
// 现在标准输出已经重定向到 output.txt
printf("这将会写入到 output.txt\n");
// 关闭原始文件描述符 fd
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
// 继续程序的其他部分...
return 0;
}
在上面的代码中,STDOUT_FILENO
是标准输出的文件描述符编号(通常是 1
)。open
调用用于打开(或创建)文件 output.txt
。然后 dup2
调用用于将 stdout
重定向到这个文件。之后,所有写入 stdout
的内容都会出现在 output.txt
文件中。最后,通过调用 close
来关闭原始的文件描述符 fd
。
FILE
- 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd。
来段代码在研究一下:
#include <stdio.h>
#include <string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行出结果:
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关
!
这个现象的原因在于printf
和fwrite
是库函数,它们使用了缓冲IO,而write
是系统调用,使用了非缓冲IO。
在C语言中,库函数printf
和fwrite
在输出时,会先将数据保存到缓冲区,只有在以下几种情况下才会将缓冲区的内容输出:
- 缓冲区满。
- 遇见’\n’。
而系统调用write
则会直接输出,不经过缓冲区。
当你使用fork
创建子进程时,子进程会复制父进程的所有资源,包括缓冲区
。因此,如果在fork
之前,printf
和fwrite
的输出还在缓冲区中没有被输出,那么在子进程中,这些输出会被再次输出,因此你看到printf
和fwrite
输出了两次。而write
因为是直接输出,所以只输出了一次。
如果你希望printf
和fwrite
在fork
之前就输出,可以在它们之后调用fflush(stdout)
来强制刷新缓冲区。这样,fork
时就不会复制还未输出的数据,从而避免重复输出。希望这个解释能帮到你!
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
如果有兴趣,可以看看FILE结构体:
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
_IO_FILE
是Linux系统中用于描述文件的结构,称为文件流。在程序执行时,fread
、fwrite
等标准函数需要文件流指针来指引去调用虚表函数。特殊地,fopen
等函数会进行创建,并分配在堆中。
_IO_FILE
结构体中的各个字段主要包括:
_flags
:高位是_IO_MAGIC
,其余是标志位。_IO_read_ptr
、_IO_read_end
、_IO_read_base
:这些字段与读缓冲区有关。_IO_write_base
、_IO_write_ptr
、_IO_write_end
:这些字段与写缓冲区有关。_IO_buf_base
、_IO_buf_end
:这些字段与备用区有关。_IO_save_base
、_IO_backup_base
、_IO_save_end
:这些字段用于支持备份和撤销。_markers
:标记。_chain
:指向下一个_IO_FILE
结构。_fileno
:封装的文件描述符。
理解文件系统
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。
每行包含8列:
模式
硬链接数
文件所有者
文件所属组
大小
最后修改时间
文件名
ls -l读取存储在磁盘上的文件信息,然后显示出来
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息。
上面的执行结果有几个信息需要解释清楚:
inode:为了能解释清楚inode我们先简单了解一下文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被
划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设
定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工
作。
为了说明问题,我们将上图简化:
创建一个新文件主要有一下4个操作
:
- 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。 - 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
复制到300,下一块复制到500,以此类推。 - 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。 - 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
理解硬链接
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个
inode。
这是Linux系统中创建硬链接的命令。让我们逐一解析这些命令:
touch abc
:这个命令创建了一个名为abc
的新文件。ln abc def
:这个命令创建了一个名为def
的硬链接,它链接到了文件abc
。这意味着abc
和def
实际上是同一个文件,只是名字不同。ls -li
:这个命令列出了当前目录下的文件,以及它们的inode号。在这个例子中,abc
和def
有相同的inode号(1580155),这表明它们是同一个文件的硬链接。
总的来说,这些命令创建了一个新文件abc
,然后为这个文件创建了一个硬链接def
。这两个名字实际上指向的是同一个文件。
软链接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法。
在shell中,创建软链接的命令是ln -s
¹²。以下是具体的指令:
- 创建软链接:
ln -s <源文件或目录> <软链接文件或目录>
在上述指令中,<源文件或目录>
和<软链接文件或目录>
需要替换为实际的源文件或目录的路径,以及你想要创建的软链接的路径¹²。
例如,如果你想为/bin/less
创建一个软链接/usr/local/bin/less
,你可以使用以下命令²:
ln -s /bin/less /usr/local/bin/less
这样,当你访问/usr/local/bin/less
时,实际上访问的是/bin/less
²。
acm
下面解释一下文件的三个时间:
Access 最后访问时间
Modify 文件内容最后修改时间
Change 属性最后修改时间
动态库和静态库
静态库与动态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
测试程序
/add.h/
#pragma once
int add(int x,int y);
/add.c/
#include"add.h"
int add(int x,int y)
{
return x+y;
}
/sub.h/
#pragma once
int sub(int x,int y);
/sub.c/
#include"sub.h"
int sub(int x,int y)
{
return x-y;
}
///main.c
#include<stab.h>
#include"add.h"
#include"sub.h"
int main()
{
int c = add(3,4);
printf("3+4=%d\n",c);
int d = sub(5,2);
printf("5-2=%d\n",d);
return 0;
}
生成静态库
要生成静态库,你需要将 add.c
和 sub.c
这两个源文件编译成目标文件,然后将这些目标文件打包成静态库文件。下面是生成静态库的步骤:
- 首先,编译
add.c
和sub.c
这两个源文件为目标文件。使用以下命令分别编译它们:
gcc -c add.c -o add.o
gcc -c sub.c -o sub.o
- 接下来,将这两个目标文件打包成静态库文件。使用以下命令将目标文件打包成
libmymath.a
静态库文件:
ar rcs libmymath.a add.o sub.o
这将创建一个名为 libmymath.a
的静态库文件,其中包含了 add.o
和 sub.o
这两个目标文件的内容。
- 现在,你可以在
main.c
中使用这个静态库文件。确保main.c
文件中包含了正确的头文件引用,并使用以下命令编译main.c
并链接静态库文件:
gcc main.c -L. -lmymath -o myprogram
这将生成一个名为 myprogram
的可执行文件,其中包含了 main.c
中的代码,并链接了 libmymath.a
静态库文件。
请确保在编译和链接过程中,所有的文件路径和文件名都正确无误。如果一切顺利,你应该能够成功生成静态库和可执行文件。
生成动态库:
要生成动态库,你可以按照以下步骤进行操作:
- 首先,编译
add.c
和sub.c
文件,生成对应的目标文件。使用以下命令:
gcc -c add.c -o add.o
gcc -c sub.c -o sub.o
这将分别生成 add.o
和 sub.o
两个目标文件。
- 接下来,将目标文件链接为一个动态库。使用以下命令:
gcc -shared add.o sub.o -o libmymath.so
这将把 add.o
和 sub.o
链接为一个名为 libmymath.so
的动态库。
- 生成动态库后,你可以在
main.c
中使用该库。确保在main.c
中包含正确的头文件引用:
#include <stdio.h>
#include "add.h"
#include "sub.h"
- 最后,编译
main.c
并链接动态库。使用以下命令:
gcc main.c -L. -lmymath -o myprograms
这将链接 main.c
并指定动态库的路径和名称。
现在,你可以运行 myprogram
可执行文件,它将使用动态库中的函数。
请注意,生成动态库的命令和生成静态库的命令略有不同。动态库使用 -shared
选项进行链接,而静态库使用 ar
命令进行打包。确保在编译和链接时使用正确的选项和命令。