文件的输入输出在c语言之前了解过。比如标准库的IO接口
回顾C语言的库函数
fopen,fread,fwrite,fseek,fclose这几个函数。关键在于参数的使用。
例如fopen,FILE *fopen(const char *path, const char *mode)
,在不同的模式下打开文件所能作的操作也不同。
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
r–只读 | 为了输入数据,打开一个已经存在的文本 | 文件出错 |
w–只写 | 为了输出数据,打开一个文本文件 | 建立一个新文件 |
a–追加 | 向文本文件尾添加数据 | 出错 |
rb–只读 | 为了输入数据,打开一个二进制文件 | 出错 |
wb–只写 | 为了输出数据,打开一个二进制文件 | 建立一个新文件 |
ab–追加 | 向一个二进制文件尾添加数据 | 出错 |
r±-读写 | 为了读和写,打开一个文本文件 | 出错 |
w±-读写 | 为了读和写,建立一个新的文件 | 建立一个新文件 |
a±-读写 | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
rb±-读写 | 为了读和写打开一个二进制文件 | 出错 |
wb±-读写 | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
ab±-读写 | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
先回顾一下C语言中的文件操作
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(){
int ret;
FILE *fp = NULL;
fp = fopen("./tmp.txt","r+");
if(fp == NULL){
perror("打开失败");
return -1;
}
fseek(fp, 5, SEEK_END);
char *ptr = "nihao---\n";
ret = fwrite(ptr, 1, strlen(ptr), fp);
printf("write item:%d\n",ret);
fseek(fp, 0, SEEK_SET);
char buf[1024] = {0};
ret = fread(buf, 1, 1023, fp);
perror("读取失败");
printf("read buf[%s]-[%d]",buf,ret);
fclose(fp);
return 0;
}
先打开当前路径下的tmp.txt文件以读写的方式打开,之后利用fseek函数跳转读写位置从当前文件位置的末尾开始向后五个位置跳转。然后写入数据,一个字节写入,写入长度为*ptr的字符串长度,然后再跳转到文件的开头开始读取数据读取到buf中。fread返回值是读取的数据长度。最后关闭文件。
这是C语言库函数中的使用方法。
系统调用IO接口
文件描述符和文件流指针
标准库接口使用文件流指针 *FILE
系统调用接口使用文件描述符 比如 int fd
进程中使用open函数打开某个文件,前提是需要我们将进程与文件联系起来。所以进程PCB中就有一个 *FILE
指针,这个*FILE
指针指向一个数组files_struct,这个数组内每个元素都对应了一个文件指针,文件指针指向各个FILE结构体。
文件流指针这个结构体中就包含了文件描述符,当使用标准库接口进行io,则最终是通过文件流指针找到文件描述符进而对文件进行操作
文件描述符是一个正整型数字。文件描述符实际上就是一个数组下标,当进程每打开一个文件,都会使用struct file描述这个文件,并且将描述信息添加到struct file这个结构中的file结构体数组中,并且向用户返回数组下标作为文件描述符,用户通过文件描述符对文件进行操作,再内核实际上是通过文件描述符找到文件描述信息,进而操作文件。
标准输入 标准输出 标准错误
stdin stdout stderr
0 1 2
文件描述符分配规则:最小未使用
系统调用函数
open write read close lseek
int open(const char *pathname, int flags, mode_t mode);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
off_t lseek(int fd, off_t offset, int whence);
int open(const char *pathname, int flags, mode_t mode);
pathname:文件路径名
flags:选项标志
必选项:
O_RDONLY 只读
O_WRONLY 只写
O_REWR 可读可写
可选项:
O_CREAT 文化不存在则创建,存在则打开
O_EXCL 与O_CREAT同用时,若文件存在则报错
O_APPEND 写追加模式
mode:创建文件时给定权限 (八进制数字)
mode & (~umask)
返回值:文件描述符-正整数 错误:-1
ssize_t write(int fd, const void *buf, size_t count);
fd:打开文件所返回的文件描述符
buf:要向文件写入数据
count:要写入的数据长度
返回值:实际的写入字节数 错误:-1
ssize_t read(int fd, void *buf, size_t count);
fd:打开文件所返回的文件描述符
buf:对读取到的数据进行存储的位置
count:要读取的数据长度
返回值:实际的读取字节数 错误:-1
off_t lseek(int fd, off_t offset, int whence);
fd:打开文件所返回的文件描述符
offset:偏移量
whence:偏移位置
SEEK_SET
SEEK_CUR
SEEK_END
返回值:返回当前位置到文件起始位置的偏移量
掌握知识的最好办法还是结合代码!!!
#include <stdio.h>
#inlcude <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
int main(){
//mode_t umask
//修改调用进程的文件创建权限掩码
umask(0);
int fd = open("./tmp.txt",O_RDWR | O_CREAT | O_APPEND,0777);
if(fd < 0){
perror("open error");
return -1;
}
char buf[1024] = "nihaoa~~!!";
int ret = write(fd, buf, strlen(buf));
if(ret < 0){
perror("write error");
return -1;
}
lseek(fd, 0, SEEK_SET);
memset(buf, 0x00, 1024);//对buf数组所在的内存空间全部初始化为0,初始化的长度为1024
ret = read(fd, buf, 1023);
if(ret < 0){
perror("read error");
return -1;
}
printf("read buf:[%s]\n", buf);
close(fd);
return 0;
}
运行如图,系统调用函数还是比较简单的。
标准输入输出的重定向
这里要学习一个dup2函数。
函数dup和dup2提供了复制文件描述符的功能。他们通常用于stdin,stdout或进程的stderr的重定向。
int dup2(int oldfd, int newfd);
dup2用来复制参数oldfd所指的文件描述符,并将oldfd拷贝到参数newfd后一起返回。若参数newfd为一个打开的文件描述符,则newfd所指的文件会先被关闭,若newfd等于oldfd,则返回newfd,而不关闭newfd所指的文件。dup2所复制的文件描述符与原来的文件描述符共享各种文件状态。共享所有的锁定,读写位置和各项权限或flags。
返回值:如果成功则返回新的文件描述符,否则出错返回-1.
由dup2函数返回的新文件描述符一定是当前文件描述符可用的最小值
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main(){
int fd = open("./tmp.txt",O_RDWR,0777);
dup2(fd, 1);
printf("%d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
此时输出的结果为1,因为oldfd为打开文件的文件描述符,本应该为3,但是调用了dup2函数,此时stdout关闭了,将oldfd复制进去此时在返回新的文件描述符即为1。
模拟实现minishell重定向
1、接收标准输入数据
2、解析命令(判断是否包含重定向符号 > \ >>)
3、如果包含,则认为需要输出重定向,这时候获取重定向符号后边的文件名将重定向符号替换成’\0’
4、在子进程中打开文件,将标准输出重定向到这个文件,进行程序替换
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <wait.h>
//之前写过一个关于minishell的文件,先对之前的一些进行封装函数
char buf[1024] = {0};
char *argv[32];
int argc = 0;
void do_face(){
printf("[liuyucheng@localhost]$ ");
fflush(stdout);
memset(buf, 0x00, 1024);
if(scanf("%[^\n]",buf) != 1){
getchar();
}
}
void do_parse(){
char *ptr = buf;
argc = 0;
while(*ptr != '\0'){
if(!isspace(*ptr)){
argv[argc++] = ptr;
while(!isspace(*ptr) && *ptr != '\0'){
ptr++;
}
}else{
*ptr = '\0';
ptr++;
}
}
argv[argc] = NULL;
return;
}
int main(){
while(1){
do_face();
int redirect = 0;
char *file = NULL;
char *ptr = buf;
while(*ptr != '\0'){
if(*ptr == '>'){
redirect = 1;//清空重定向
*ptr++ = '\0';
if(*ptr == '>'){
redirect = 2;
*ptr++ = '\0';
}
while(isspace(*ptr) && *ptr != '\0'){
ptr++;
}
file = ptr;
while(!isspace(*ptr) && *ptr != '\0'){
ptr++;
}
*ptr = '\0';
}
ptr++;
}
do_parse();
int pid = fork();
if(pid < 0){
exit(-1);
}else if(pid == 0){
if(redirect == 1){
int fd = open(file, O_CREAT | O_WRONLY | O_TRUNC, 0664);
dup2(fd, 1);
}else if(redirect == 2){
int fd = open(file, O_CREAT | O_WRONLY | O_TRUNC, 0664);
dup2(fd, 1);
}
execvp(argv[0], argv);
exit(0);
}
wait(NULL);
}
return 0;
}
Linux ext2文件系统
注:图中应为inode。
上图是一个磁盘文件系统的内置。
每个分区都有一个文件系统,不同的分区拥有不同的文件系统。
什么是inode?inode里面包含了大小,权限,用户,时间,块位置
利用指令ls -i
可以查看inode节点号
存储文件的流程
通过inode bitmap在inode table(表结构)找到空闲的inode节点,通过data bitmap在数据块区域找到空闲数据块,将数据块位置信息,记录到inode节点中,将文件数据写入到数据块中;将文件名和inode节点名写入父目录中。
目录文件中:存放了一张目录下有什么文件的表,表中记录了文件名。inode节点号—>目录项
当我们 cat ./a.txt
输出文件内容时,在当前目录文件中查找文件名信息,通过文件名获取inode节点号,通过inode节点号,找到inode节点,进而访问数据块,读取数据进行打印。
软硬链接
创建硬链接:ln a.txt b.txt
创建软链接:ln -s a.txt a.soft
硬链接是一个文件的另一个名字,跟源文件并没有什么区别,----inode节点号相同
软链接是一个独立的文件,像是一个文件的快捷方式,----inode节点号不同
删除源文件,软链接失效---->通过记录的源文件名路径查找源文件数据;
硬链接无影响---->通过inode节点找文件只是链接数-1
软链接可以针对目录进行创建,硬链接不可以
软链接可以跨分区建立,硬链接不可以
静态库与动态库
静态库:名字一般为libxxx.a,编译时会整合到可执行程序中。程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库 。优点是运行时不需要外部函数库支持,缺点是编译后程序较大,一旦静态库改变,程序需要重新编译。
动态库:名字一般为libxxx.M.N.so,M为主版本号,N为副版本号 ,程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。优点是运行时,有需要时才动态调用外部库中的函数,节省空间,缺点是运行环境中必须提供相应的库,动态库更新升级方便。
建立静态库
gcc -fPIC -c b.c -o b.o
产生位置无关代码
生成静态库gcc -c b.c -o b.o
ar -cr libmytest.a b.o
生成静态库
gcc选项:
-fPIC:产生位置无关代码
--share:生成一个共享库而不是可执行程序
ar:静态库打包所有命令
-c 创建
-r 替换
建立动态库
生成动态库gcc -fPIC -c b.c -o b.o
gcc --share b.o -o libmytest.so
生成动态库
库的使用
动态库—>libmytest.so 静态库—>libmytest.a
同名动态和静态库,先链接动态库
链接库的时候:gcc a.c -o main -lmytest。如果报错,找不到库(链接库的查找路径----库的查找路径)
库链接的时候和运行加载的时候都需要在指定目录下
库文件的默认查找路径/lib64 /usr/lib64
设置环境变量:LIBRARY_PATH=.(库的链接路径)------>使用选项 ’-L‘
因为gcc默认是动态链接—因此优先使用动态库生成可执行程序
注意!
通常我们自己链接静态库生成可执行程序的时候,并不是使用-static静态链接,而是将静态库放到指定路径下,然后直接使用gcc -L选项指定库的链接路径链接静态库生成可执行程序
-static:作用是可执行程序使用静态链接生成,不依赖任何动态库
关于基础IO的总结
最重要的还是区分系统调用和库函数调用的区别和用法。在不同的语言中有着不同的用法,Linux下的使用可能还要考虑到进程的创建,程序替换等操作。
还有就是文件系统的理解与inode节点的作用,磁盘的分配有着独特的规则,通过inode来调用分配空间的使用。