目录
前言
用库进行文件操作
文件描述符
理解Linux一切皆文件
缓冲区
认识缓冲区
缓冲区缓冲策略
磁盘结构
磁盘分区
软链接和硬链接
硬链接本质
软连接本质
动态库和静态库进阶
写一个静态库
动态库的产生和使用
动静态库的加载
总结:
前言
在我们了解基础文件IO时,我们首先需要接受下列事实:
用库进行文件操作
任何语言都有进行文件操作的函数,不管是C/C++还是python、shell都有自己的库函数进行文件操作,但是我们知道文件操作的实质是对磁盘进行操作,这是操作系统做的是,所以不管是什么语言,文件操作都是调用的系统文件操作的接口。
如图可以看出,进程想要管理被打开的文件,在PCB中有指针指向一个结构体——文件描述符表,内部包含一个指针数组指向打开文件的属性结构体,实现对文件的操作。
文件描述符
文件描述符(fd)分配规则:从小到大,按照循序寻找最小且没有被占用的fd。
重定向的本质就是:上层用的fd不变,在内核中更改fd对应的struct files*的地址。
这么一来,标准输出到显示屏中的内容都会被写到新文件之中。
我们可以借助于系统调用接口dup2实现:
只要调用,就会用oldfd对应的文件对象替代newfd对应的文件对象,也就是原来newfd对应的文件已经关闭,对这个文件描述符的任何操作都转化为对oldfd对应的文件的操作。
接下来是实现重定向:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<string.h>
#include<ctype.h>
#include<errno.h>
#define LINESIZE 1024
#define ARGNUM 64
#define NONE_REDIR 0
#define OUT_REDIR 1
#define APPENED_REDIR 2
#define IN_REDIR 3
#define DELBLANK(command,start) do{\
while(isspace(command[start])) ++start;\
}while(0)
char* filename = NULL;
int redir_type = 0;
void CommandCheck(char* command)
{
int start = 0;
int end = strlen(command);
while(start < end)
{
if('<' == command[start])
{
redir_type = IN_REDIR;
command[start] = '\0';
++start;
DELBLANK(command, start);
filename = command + start;
break;
}
else if('>' == command[start])
{
command[start] = '\0';
++start;
if('>' == command[start])
{
redir_type = APPENED_REDIR;
++start;
DELBLANK(command, start);
filename = command + start;
}
else
{
redir_type = OUT_REDIR;
DELBLANK(command, start);
filename = command + start;
}
break;
}
++start;
}
}
int main()
{
char* myarg[ARGNUM];
char command[LINESIZE];
int status = 0;
while(1)
{
filename = NULL;
redir_type = NONE_REDIR;
printf("[用户名@ 主机名 当前地址#] ");
fflush(stdout);
//接受指令 分割指令
char* c = fgets(command,LINESIZE - 1,stdin);
assert(c != NULL);
(void)c;
command[strlen(command) - 1] = 0;
CommandCheck(command);
myarg[0] = strtok(command, " ");
int i = 1;
while(myarg[i++] = strtok(NULL," "));
//内建指令
if(myarg[0] != NULL && strcmp(myarg[0],"cd") == 0)
{
if(myarg[1] != NULL)
{
chdir(myarg[1]);
}
continue;
}
if(myarg[0] != NULL && strcmp(myarg[0],"echo") == 0)
{
if(myarg[1] != NULL && strcmp(myarg[1],"$?") == 0)
{
printf("%d\n",status>>8&0xFF);
}
else if(myarg[1] != NULL)
{
printf("%s\n",myarg[1]);
}
continue;
}
//执行指令
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
switch(redir_type)
{
case NONE_REDIR:
break;
case OUT_REDIR:
case APPENED_REDIR:
{
int flag = O_WRONLY | O_CREAT;
if(redir_type == APPENED_REDIR)
{
flag |= O_APPEND;
}
else
{
flag |= O_TRUNC;
}
int fd = open(filename, flag, 0666);
if(fd < 0)
{
perror(">/>>");
exit(errno);
}
dup2(fd, 1);
}
break;
case IN_REDIR:
{
int fd = open(filename, O_RDONLY);
if(fd < 0)
{
perror("open");
exit(errno);
}
dup2(fd, 0);
}
break;
default:
{
printf("bug?\n");
exit(1);
}
break;
}
int exeret = execvp(myarg[0],myarg);
if(exeret == -1)
{
exit(10);
}
exit(1);
}
waitpid(id,&status,0);
}
return 0;
}
理解Linux一切皆文件
外设的功能就是IO,也就是读和写,但是不同的硬件读写方式不一样,所以就需要有不同的函数方法,但是在操作系统管理外设时是将不同的外设看成文件统一管理的,在files对象中有两个指针分别指向这个外设所使用的读写方法,实现对不同外设的统一管理。
缓冲区
认识缓冲区
我们通过一个简单的实验现象来观察缓冲区:
同样的代码,将内容写入标准输出时和写入文件时的结果完全不同,当写入标准输出显示在显示屏上的结果符合预期,但是写入文件中的结果出乎预料,系统调用被写入一次,但是函数调用被写入两次。
这是由于在重定向到文件中时,函数调用的内容被写入到缓冲区,并且文件打开缓冲区使用的规则是全缓冲 ,即缓冲区满之后才把内容做刷新。
并且这个缓冲区一定不在内核之中,如果在内核中系统调用接口也应该打印两次,这个缓冲区是一个用户级语言层面给我们提供的缓冲区。
缓冲区缓冲策略
缓冲区的缓冲策略一共有三种:
a 立即刷新,无缓冲
b 按行刷新,行缓冲
c 缓冲区满刷新,全缓冲
还有两种情况,用户主动刷新和退出之前会有对缓冲区进行刷新。
我们来写一个简易版的缓冲区:
#pragma once
#include<sys/stat.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
#define SYNC_NONE 1
#define SYNC_LINE 2
#define SYNC_FULL 4
typedef struct FILE_
{
int _fd;
int _buftype;
char _buffer[1024];
int _cap;
int _size;
}FILE_;
FILE_ * fopen_(const char* file_name, char mode);
void fwrite_(const void* str, int num, FILE_* fp);
void fclose_(FILE_* fp);
void fflush_(FILE_* fp);
#include"mystdio.h"
FILE_* fopen_(const char* file_name, char mode)
{
FILE_* ret = (FILE_*)malloc(sizeof(FILE_));
assert(ret);
int flags = 0;
if(mode == 'r')
{
flags = O_RDONLY;
ret->_fd = open(file_name, flags);
}
else if(mode == 'w')
{
flags = O_WRONLY | O_CREAT | O_TRUNC;
ret->_fd = open(file_name, flags, 0666);
}
else if(mode == 'a')
{
flags = O_WRONLY | O_CREAT | O_APPEND;
ret->_fd = open(file_name, flags, 0666);
}
else{
free(ret);
return NULL;
}
ret->_buftype = SYNC_LINE;
memset(ret->_buffer, 0, 1024);
ret->_cap = 1024;
ret->_size = 0;
return ret;
}
void fwrite_(const void * str, int num, FILE_* fp)
{
memcpy(fp->_buffer,str, num);
fp->_size += num;
if(fp->_buftype == SYNC_LINE && num + fp->_size < fp->_cap)
{
if(fp->_buffer[fp->_size - 1] == '\n')
{
write(fp->_fd, fp->_buffer, fp->_size);
fp->_size = 0;
}
}
else if(fp->_buftype == SYNC_FULL)
{
if(fp->_size == fp->_cap)
{
write(fp->_fd, fp->_buffer, fp->_size);
fp->_size = 0;
}
}
}
void fclose_(FILE_* fp)
{
fflush_(fp);
close(fp->_fd);
free(fp);
}
void fflush_(FILE_* fp)
{
if(fp->_size != 0)
{
write(fp->_fd, fp->_buffer, fp->_size);
fp->_size = 0;
}
}
#include"mystdio.h"
#include<stdio.h>
int main()
{
FILE_* fp = fopen_("./log.txt", 'w');
int cnt = 10;
while(cnt)
{
char str[64];
sprintf(str,"cnt = %d \n",cnt);
--cnt;
fwrite_(str,strlen(str),fp);
sleep(1);
}
fclose_(fp);
}
上面模拟的是行缓冲,标准输出因为是给用户看的,所以需要及时刷新,使用行缓存,而对文件进行读写则使用全缓冲。
磁盘结构
磁盘结构的内容可以参看:https://blog.csdn.net/cyynid/article/details/129118760?spm=1001.2014.3001.5502
这里大佬写的博客已经比较清楚了,这里只补充一点:
使用逻辑抽象主要是为了方便管理,以及降低代码和硬件之间的耦合(毕竟有些硬盘用的可不是CHS)。
根据局部性原理:在最近的将来将用到的信息很可能与正在使用的信息在空间地址上是临近的。
虽然磁盘对应的访问的基本空间是512字节,这依然显得太小了。在os内部的文件系统中定制了多个扇区的读写——1 / 2 / 4 kb。
内存划分为4kb大小的空间——页框。
磁盘中的文件尤其是可执行文件划分为4kb大小的空间——页帧。
磁盘分区
Surper Block : 保存了整个文件系统的信息,备份防止损坏。
Group Descriptor Table : 快组描述表(GDT)对应分组的宏观描述信息,Inode table 以及Date block的使用情况。
Block bitmap : Date blocks的位图,对应Date blocks 的占用情况。
Inode Bitmap : Inode table的位图,对应Inode table 的占用情况。
Inode Table : 保存了分组内部所有(可用的 + 已使用的)Inode, 一个inode占128字节.
Date Blocks : 保存了分组内部所有文件的数据块。
文件 = 属性 + 内容。
几乎所有属性都储存在Inode 中,文件名不在Inode中。
文件内容都储存在Date blocks 中。
每个inode在文件系统中都有一个编号(下图左侧):
补充一点:目录也是一个普通文件,目录文件内容是当前文件下文件名和Inode编号的映射关系。
软链接和硬链接
我们先来见见创建软硬链接的过程:
分别对同一个文件—— myfile.txt 建立软连接和硬链接之后我们可以发现以下几点:
1.软连接和原文件的inode编号不同
2.硬链接和原文件的inode编号相同
3.添加硬链接之后,文件属性中有一个值从1变为了2
4.软硬链接都可以打开文件
下面我们根据现象来揭开本质:
软硬链接的区别是什么?
区别是软连接有独立的Inode,而硬链接没有独立的Inode.
硬链接本质
硬链接没有独立的inode,所以硬链接没有新增文件,只是新增了新的文件名与原文件Inode编号相映射的关系,inode内部记录了被指向(映射)的数目(硬链接数),rm是减硬链接数,只有为0时才真正被删除。
软连接本质
软链接类似于Windows下的快捷方式,有独立的Inode,是一个新的文件,内部为一个指向原文件的路径(只认名字,不认inode编号),也就是说只要是在这个路径下这个文件名的文件,软连接就能打开,与inode无关。
目录中的. 和 .. 是硬链接,操作系统不允许用户给目录建立硬链接。
补充:三个时间
Access 最后访问时间
Modify 文件内容最后修改时间
Change 属性最后修改时间
动态库和静态库进阶
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文 件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个 过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚 拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
写一个静态库
-c 生成 .o 文件(可重定位目标二进制文件)
我们来看看.o文件是怎么用的:
//add.h文件
#include<stdio.h>
int add(int a, int b);
//add.c文件
#include "add.h"
int add(int a, int b)
{
return a + b;
}
要使用库就得有.h文件,因为我们自己写的代码(main.c)引用了头文件。
其实.o文件和库的用法已经有些相似了,库其实就是将许多.o文件打包在一起,形成一个库。
形成静态库的命令:
ar -rc xxx.a xxx.o
通过makefile文件来看看静态库是如何产生的:
再将形成的库打包发送到要使用的目录下。
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
要调用库有下列几种方法:
1.定向找到头文件路径,定向找到库文件路径,链接第三方库需指明库名:
-I include的第一个字母
-L lib的第一个字母
-l 库文件名去前缀和后缀
2.将头文件和库文件分别拷贝到/usr/include/和/lib64/中去。
这就是安装的过程,第三方库即使被安装也需要指明文件名。
动态库的产生和使用
动态库的产生和静态库有一些不一样:
1. 生成.o文件时,要多加一个选项 -fPIC(位置无关码,相应的函数在库文件中的相对位置)。
2.生成.so文件时多加一个 -shared
但是直接像静态库一样调用动态库会失败,因为动态库在运行时也需要知道库的位置。
有四个方法可以解决这个问题:
1.改环境变量(重新启动后失效)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:路径
2.将动态库路径添加到/etc/ld.so.conf.d/路径下的任意一个.conf文件 + 命令行指向 ldconfig即可
且重启后仍然有效
3.在当前目录下建立软链接
注意:链接名得和库名一样
4.这种方法和静态库的第二种方法相同
动静态库的加载
静态库中的代码在可执行程序中,储存在代码区(调用时使用绝对地址)
调用动态库函数时,使用位置无关码,确定函数定义在动态库中的相对偏移位置(偏移量),动态库被写入共享区,通过偏移量调用相应函数。
总结: