一、系统I/O
(一)与文件相关的系统调用:
1、open:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);
参数:
- pathname:要打开或创建的目标文件
- flags:打开文件时,可以传入以下选项和选项的异或运算,构成flags
O_RDONLY:只读方式打开
O_WRONLY:只写方式打开
O_RDWR:读写方式打开
以上三种方式,必须包含一个且只能包含一个。
O_APPEND:追加写
O_CREAT:若文件不存在,就创建。需要使用mode选项,来指明新文件的访问权限。
- mode:当新文件创建时,指明新文件的访问权限。当flags有O_CREAT选项时,必须指定权限。
返回值:打开成功返回打开文件的文件描述符;打开失败则返回-1.
2、close:
#include <unistd.h> int close(int fd);
参数:fd:文件描述符
返回值:成功返回0,;失败返回-1
3、read:
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count);
含义:从fd中将count个字节的数据到buf中
返回值:若成功,返回读取的字节数;若失败,返回-1.
4、write:
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
含义:将buf中的数据的count个字节的数据写入fd的文件中。
返回值:若成功,返回写入的字节数;若失败,返回-1.
eg:从标准输入读入数据并写到文件中。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("test", O_RDWR | O_CREAT, 0644); if(fd < 0) { perror("open"); return -1; } char buf[1024] = {0}; ssize_t rret = read(0, buf, 12); if(rret < 0) { perror("read"); return -1; } ssize_t wret = write(fd, buf, 12); if(wret < 0) { perror("write"); return -1; } close(fd); return 0; }
(二)文件描述符
上面介绍系统IO时,提到了文件描述符。那么文件描述符究竟是什么呢?
我们知道fopen等C语言的库函数是open等系统调用的一层封装。
通过open等函数,我们已经了解到文件描述符是一个整数,那么这些整数有什么含义呢?
我们写一个函数实现打开一个文件,打印出它的文件描述符:
int main() { int fd = open("test", O_RDWR | O_CREAT, 0644); if(fd < 0) { perror("open"); return -1; } printf("fd:%d\n", fd); close(fd); return 0; }
发现打印出来的是3,那0,1和2呢?
答:Linux进程默认情况下会有三个缺省打开的文件描述符,0,1和2分别是标准输入,标准输出,和标准错误。它们对应的硬件设备分别是键盘,显示器,显示器。
int main() { char buf[1024] = {0}; //从标准输入读取数据 ssize_t ret = read(0, buf, sizeof(buf)); if(ret < 0) { perror("read"); return -1; } buf[ret] = '\0'; //把buf中的数据写进标准输出 ssize_t rret = write(1, buf, strlen(buf)); if(rret < 0) { perror("write into stdout"); return -1; } //把buf中的数据写进标准输出 ssize_t rrret = write(2, buf, strlen(buf)); if(rrret < 0) { perror("write into stderr"); return -1; } return 0; }
从结果也可以看出来,fd为0,1和2分别代表的含义:
那么文件描述符究竟是如何找到要打开的文件,它的原理是什么呢?
答:我们在进程中要打开文件,就要把进程和文件联系起来。进程的PCB中就保存有这样一个指针,指向一张表files_struct,该表保存的是该进程打开的文件指针,每一个文件指针又执行一个个结构体,结构体中保存着打开文件的inode元信息。通过以上,一步一步可以找到文件。
注意:
文件描述符的分配是从0开始,找到当前未使用的最小下标的位置,作为新的文件描述符。
eg:如果我们关闭默认的标准输出,那么新创建的文件描述符就会从1开始
int main() { close(1); char buf[1024] = "hello bit"; int fd = open("./mytest", O_WRONLY | O_CREAT, 0644); if(fd < 0) { perror("open"); return -1; } printf("fd:%d\n", fd); fflush(stdout); int ret = write(fd, buf, strlen(buf)); if(ret < 0) { perror("write"); return -1; } close(fd); return 0; }
(三)重定向
上面关闭标准输入,新打开的文件描述符就会是1,就是一个重定向的过程。把本来应该写入标准输出的内容写到了mytest文件中。怎么理解重定向呢?我们通过一张图来理解:
(四)缓冲
观察下面的函数运行结果:
int main() { printf("hello printf\n"); char buf1[1024] = "hello fwrite\n"; fwrite(buf1, sizeof(char), strlen(buf1), stdout); char buf2[1024] = "hello write\n"; write(1, buf2, strlen(buf2)); fork(); return 0; }
把内容从写道标准输出该为写到指定文件,或者进行重定向到文件,观察结果:
从上面的运行结果中我们发向同样的代码,运行出来结果不同,重定向后hello printf和hello fwrite都多打印了一次,这是为什么呢?
答:write是系统调用,无缓冲区;而printf和fwrite都是C语言函数,当写入显示器时,是以行缓冲方式工作,遇到‘\n’时刷新缓冲区,将结果写到指定文件。当重定向到普通文件时,变为全缓冲方式工作,进程退出时,才会刷新缓冲区。
所以,write无缓冲区,只写入一次;而printf和fwrite从行缓冲变为全缓冲,fork又拷贝了父进程的代码和数据,还包括缓冲区中的内容,父子进程退出时,都刷新一次缓冲区,就造成了上面写入2次的情况。
二、理解文件系统
(一)文件系统
我们可以用ls -l, ll或stat来查看文件信息:
用stat查看文件更多详细信息时,发现有两个属性必须要先理解文件系统才能理解。
让我们先简单理解下文件系统:
创建一个新文件,内核先找到一个空闲的节点,把文件信息记录到其中;如图中,假设创建的文件需要存储在三个内存块,内核找到了三个空闲快300、500和800,按顺序将内核缓冲区的第一块数据复制到300,下一块复制到500,依次进行;i节点会保存使用到的内存块编号;
那么Linux如何在当前目录中记录这个文件呢?内核将入口(17711350 abc)添加到目录文件;inode和文件名之间的对应关系将文件和文件内容和属性联系在一起。
(二)硬连接
硬链接就是使多个文件名映射到同一个inode。
观察硬链接数变为了2:
当删除其中一个文件时,会删除要删除的文件,并将硬链接数-1:
(三)软链接
硬链接是通过inode引用另一个文件;而软连接是通过名字引用另外一个文件。
三、动态库和静态库
(一)静态库
程序在编译连接的时候将库的代码链接到可执行文件中,运行时就不再需要静态库了。
//add.h #pragma once int add(int a, int b); //add.c #include "add.h" int add(int a, int b) { return a+b; } //main.c #include "add.h" int main() { int a = 10; int b = 29; int ret = add(a, b); printf("%d+%d=%d\n", a, b, ret); return 0; }
main:main.c libadd.a gcc $^ -o $@ libadd.a:add.o ar -rc libadd.a add.o //生成静态库 add.o:add.c gcc -c $^ -o $@ .PHONY:clean clean: rm add.o libadd.a main
(二)动态库
程序在运行的时候才会去链接动态库中的程序,多个程序共享使用库的代码。
在可执行文件开始执行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程叫做动态链接。