一、系统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
(二)动态库
程序在运行的时候才会去链接动态库中的程序,多个程序共享使用库的代码。
在可执行文件开始执行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程叫做动态链接。