至今所学的文件操作,无外乎读取和写入;而文件包含:文件本身所具有的属性,和文件的内容。而对文件的操作,都是进程对文件的操作。语言层库函数对硬件的操作必须经过OS的系统调用。
几乎所有的语言层面都默认打开这些接口,因为都有输入输出的需求,便于语言刚开始的学习上手使用;否则会是这样的场景:在学习这门语言之前呢,铁子们先来学一下打开文件…
fopen --> open
fclose --> close
fread --> read
fwrite --> write
cwd
ps axj | greap t1
ll /proc/t1ID
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
把 ptr 所指向的数组中的数据写入到给定流 stream 中。
char * buf[1024];
int wf = fwrite(buf,sizeof(buf),1,stdout);
w
写入 :每次写入都是重新写入;意味着之前的文件内容,会被清空。打开文件的 a
写入:追加写入,不清空原始文件内容,在文件最后进行写入,数据增多的过程。stdin键盘文件
,stdout / stderr显示器文件
。这三个流的类型都是FILE*
, fopen返回值类型,文件指针。int fprintf(FILE *stream, const char *format, ...)
发送格式化输出到流 stream 中。如果成功,则返回写入的字符总数,否则返回一个负数。fprintf(stdout,"%d , %c\n",24,a);
open
函数说明:(1) int open(const char *pathname, int flags);
(2) int open(const char *pathname, int flags, mode_t mode);
fd (file descriptor)
,值大于或等于0;失败,-1。pathname
是文件路径,可以是相对路径(即不以 “/” 开头),也可以是绝对路径(即以 “/” 开头)。flags
:宏标志位,使用时包含一种访问模式: O_RDONLY
(只读)、O_ WRONLY
(只写)或 O_RDWR
(读写)。mode
:文件的权限设置。当参数 flags
指定标志位 O_CREAT
或 O_TMPFILE
的时候,必须指定参数 mode。可以用八进制文件掩码设置。O_CREAT
与上述三种任意一种访问模式进行 ‘或’ 运算.。 flags
:宏标志位,这些参数只占一个int整形中的一个比特位。读:00;写:01;…int fd = open("t1.txt",O_ WRONLY|O_CREAT,0644);
if(fd<0)
{
perroe("open");
return 1;
}
close(fd)
write
:函数说明:ssize_t write(int fd, const void *buf, size_t count);
int fd = open("t1.txt",O_ WRONLY|O_CREAT,0644);
if(fd<0)
{
perroe("open");
return 1;
}
const char* msg = "hello write\n";
write(fd,msg,strlen(msg));
close(fd);
read
函数:ssize_t read(int fd, void *buf, size_t count);
char buf[1024];
ssize_t s = read(fd,buf,sizeof(buf)-1)
if(s>0)
{
buf[s]='\0';
printf("%s\n",buf);
}
O_APPEND
:追加写入int fd = open(t1.txt,O_WRONLY|O_APPEND);
fd 是连续的整数;输入输出错误占了0,1,2。操作系统会把进程打开的多个文件管理起来。文件本身是一个 struct file
的结构,内含有读写方法,和内核缓冲区。OS则用打开的文件指针通过struct file_struct
内的一个文件指针数组的方式进行管理。
用户层看到的fd,其实是系统中维护进程和文件对应关系的数组下标,也就是说进程是用PCB里的一个指针这样的接口进行所有的文件操作。缓冲区里的数据通过文件描述符写入对应的目标文件中。
FD的使用规则:找到当前没有被使用的最小的一个下标,作为新的文件描述符。 close
现在的含义代表: 断开 struct file_struct
里对应下标的文件指针指向,随后那块文件资源由OS接受处理,释放资源或加入闲置队列。
所有语言层面的默认打开文件,都是底层系统支持的,默认一个进程运行的时候,就打开了0,1,2.
/* open file information */
struct files_struct *files;
//缓冲区相关
/* 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. */
....
int _fileno; //封装的文件描述符
printf("%d\n",stdin->_fileno);
...
int main()
{
close(1);
int fd =open("t1.txt",O_CREAT|O_WRONLY,0644);
..
printf("hello close(%d)\n",fd);
fflush(1);//需要强刷一下
return 0;
}
arr[1]
的内容指向了新开的 t1.txt 的文件结构体,因此 printf 输出到了 新文件的缓冲区里。这就叫做输出重定向。\n
不刷新到显示器上呢?
语言缓冲区 vs 内核缓冲区
printf("hell C buffer !\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
const char* msg = "hello write\n";
write(1,msg,strlen(msg));
fork();
//假如 ./t1 > log.txt
//则父子进程都会打印语言区的缓冲区数据。内核缓冲区的只刷新一次。
内核缓冲区向显示器文件写入,行刷新;父子进程像普通文件写入,全缓冲刷新;临近进程退出时,父子进程都会把缓冲区的数据刷新出去,触发了写实拷贝的现象。
因为手动关闭stdout,太挫了,所以系统提供了重定向接口函数:int dup2(int oldfd, int newfd);
当调用dup2(int oldfd,int newfd)之后,若newfd原来已经打开了一个文件,则先关闭这个文件,然后newfd和oldfd指向了相同的文件;若newfd原来没有打开一个文件,则newfd直接指向和oldfd指向相同的文件。
关于dup2()函数
int fd = open("t1.txt",O_WRONLY);
dup2(fd,1);
....
显示器文件就指向了 arr[1] ---> struct file t1.txt
Linux下一切皆文件
对于OS而言,把硬件抽象出来,用一样的名字表示;打开的文件都会创建一个struct_file
的数据结构,通过双向链表链接起来…。为什么数据不直接拷贝进内核?有权限切换等备工作,花费资源和时间;相反直接放在用户层缓冲区,解耦合提高效率。所有对数据的操作必须先加载到内存,cpu处理,通过OS,刷新到外设上。
10几年前,是机械硬盘的时代,虽然现在是SSD,速度快了一个数量级;但在cpu面前是很慢。所以书上还是以机械硬盘为例。机械硬盘的一个扇区是512Byte。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。“块"的大小,最常见的是4KB,即连续八个 sector(扇区)组成一个 block。
进程向外设写数据时,是等内存的一个页框(站在OS角度看待内存的使用单位 4KB)写满了,OS在联系对应的外设驱动将数据刷新出去。
严格地说,文件系统是一套实现了数据的存储、分级组织、访问和获取等操作的抽象数据类型(Abstract data type)多个扇区合并在一起称为块儿组group block,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。
Linux 下支持多种文件系统。将磁盘这样的块设备抽象为线性的空间,管理好一个区,其他区就是一样的管理方式。
超级块(Super Block):存放文件系统本身的结构信息。包括 inode 和 block 的总量、已经使用量和剩余量,以及文件系统的格式和相关信息等。
blocks:多个4KB(扇区*8)大小的集合,保存的都是特定文件的内容
inode Table:包含了文件系统的所有文件列表。一个inode(节点)是在一个表项里,包含文件数据(元数据)。每个文文件有自己的一个inode(结构体)
BlockBitmap:假设有10000+个blocks,10000+比特位:比特位和特定的block是一一对应的,其中比特位为1,代表该block被占用,否则表示可用
inode Bitmap:假设有10000+个inode结点,就有10000+个比特位,比特位和特定的inode是一一对应的,其中比特位为1表示inode被占用,否则表示可用。
Group Descriptor Table(GDT):快组描述符,这个快组多大,已经使用多少了,有多少个inode,已经占用了多少个,还剩多少,一共有多少个block,使用了多少……
这些都是能够让一共文件的信息可追溯,可管理 格式化
inode Table
inode
通常情况下,文件系统会将文件的实际内容和属性分开存放:不仅如此,inode 中还记录着文件数据所在 block 块的编号; 文件的实际内容保存在 block 中(数据块),类似衣柜的隔断,用来真正保存衣物。每个 block都有属于自己的编号。当文件太大时,可能会占用多个 block 块。
inode
的结构体定义如下struct inode {
struct hlist_node i_hash; /* 哈希表 */
struct list_head i_list; /* 索引节点链表 */
struct list_head i_dentry; /* 目录项链表 */
unsigned long i_ino; /* 节点号 */
atomic_t i_count; /* 引用记数 */
umode_t i_mode; /* 访问权限控制 */
unsigned int i_nlink; /* 硬链接数 */
uid_t i_uid; /* 使用者id */
gid_t i_gid; /* 使用者id组 */
kdev_t i_rdev; /* 实设备标识符 */
loff_t i_size; /* 以字节为单位的文件大小 */
struct timespec i_atime; /* 最后访问时间 */
struct timespec i_mtime; /* 最后修改(modify)时间 */
struct timespec i_ctime; /* 最后改变(change)时间 */
unsigned int i_blkbits; /* 以位为单位的块大小 */
unsigned long i_blksize; /* 以字节为单位的块大小 */
unsigned long i_version; /* 版本号 */
unsigned long i_blocks; /* 文件的块数 */
unsigned short i_bytes; /* 使用的字节数 */
spinlock_t i_lock; /* 自旋锁 */
struct rw_semaphore i_alloc_sem; /* 索引节点信号量 */
struct inode_operations *i_op; /* 索引节点操作表 */
struct file_operations *i_fop; /* 默认的索引节点操作 */
struct super_block *i_sb; /* 相关的超级块 */
struct file_lock *i_flock; /* 文件锁链表 */
struct address_space *i_mapping; /* 相关的地址映射 */
struct address_space i_data; /* 设备地址映射 */
struct dquot *i_dquot[MAXQUOTAS]; /* 节点的磁盘限额 */
struct list_head i_devices; /* 块设备链表 */
struct pipe_inode_info *i_pipe; /* 管道信息 */
struct block_device *i_bdev; /* 块设备驱动 */
unsigned long i_dnotify_mask; /* 目录通知掩码 */
struct dnotify_struct *i_dnotify; /* 目录通知 */
unsigned long i_state; /* 状态标志 */
unsigned long dirtied_when; /* 首次修改时间 */
unsigned int i_flags; /* 文件系统标志 */
unsigned char i_sock; /* 可能是个套接字吧 */
atomic_t i_writecount; /* 写者记数 */
void *i_security; /* 安全模块 */
__u32 i_generation; /* 索引节点版本号 */
union {
void *generic_ip; /* 文件特殊信息 */
} u;
};
如图所示:文件系统先格式化出 inode 和 block 块,假设某文件的权限和属性信息存放到 inode 4 号位置,这个 inode 记录了实际存储文件数据的 block 号有 4 个,分别为 2、7、13、15,由此,操作系统就能快速地找到文件数据的存储位置。
[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 //8个扇区
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
文件的三个时间:Access 最后访问时间。有时候看到访问了时间没更新,是因为内核提升效率,降低访问时间的更新频率。Modify 文件内容最后修改时间;Change 属性最后修改时间。文件的内容修改影响文件属性的修改。修改属性不一定影响内容时间。
寻找当前目录
OS怎么找到当前目录的呢?系统也是一级一级找的,第一次找到后会把路径缓存起来;大多数OS目录下,不允许存在相同的文件名,大多数文件系统也是相互借鉴的;文件名作为K值索引对应的 inode ,进而可以找到对应的文件内容。
打开文件
表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:实际上,系统内部这个过程分成三步:首先,目录树是提前加载到内存的。系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。
删除文件
删除一个文件做了什么呢?清楚文件两个位图的信息,1变0,再把数据块儿中的映射关系去掉。创建文件时,则是块儿组里在 inode 位图中,找一个位置,在 inode 表中分配一个 inode 保存文件属性信息;在数据块位图中找到一块儿空间,分配相应的数据块儿。如果是空目录,就不用分配数据块儿。
联系平时实践,大家格式化硬盘(U盘)时发现有:快速格式化和底层格式化。快速格式化非常快,格式化一个32GB的U盘只要1秒钟,普通格式化格式化速度慢。这两个的差异?其实快速格式化就是只删除了U盘中的硬盘内容管理表(其实就是inode),真正存储的内容没有动。这种格式化的内容是有可能被找回的。
软连接
一方面避免了直接把可执行程序文件暴露在外面;一方面便捷了在外层使用执行程序,避免路径太深。
[saul@VM-12-7-centos tt730]$ ln -s t1.x t1.soft
[saul@VM-12-7-centos tt730]$ ls -l
total 0
lrwxrwxrwx 1 saul saul 4 Jul 31 20:48 t1.soft -> t1.x
-rw-rw-r-- 1 saul saul 0 Jul 31 20:47 t1.x
硬链接
硬链接的文件,没有自己独立的 inode 编号;是在同一个文件inode编号的block下创建一个:与源文件属性和内容相同的新文件名和 inode 之间的映射关系。类似对文件的引用。
当前路径下的 . 便是一个硬链接 ,所以看到刚创建一个目录,显示的硬链接数就是2。
[saul@VM-12-7-centos tt730]$ touch t1.x
[saul@VM-12-7-centos tt730]$ ll
total 0
-rw-rw-r-- 1 saul saul 0 Jul 31 20:54 t1.x //没有硬链接时 1
[saul@VM-12-7-centos tt730]$ ln t1.x t1.hard
[saul@VM-12-7-centos tt730]$ ll
total 0
-rw-rw-r-- 2 saul saul 0 Jul 31 20:54 t1.hard //硬连接数都改变了
-rw-rw-r-- 2 saul saul 0 Jul 31 20:54 t1.x
[saul@VM-12-7-centos tt730]$ rm t1.x
[saul@VM-12-7-centos tt730]$ ll
total 0
-rw-rw-r-- 1 saul saul 0 Jul 31 20:54 t1.hard //硬连接数减一..
使用别人顶尖工程师的代码为了提高开发效率和鲁棒性。使用别人功能的三个途径:库、开源代码、基本的网络功能调用。
一般为更好地支持开发,第三方或者语言库,都必须提供两个库,一个静态库,一个动态库,方便程序员根据需要进行bin的生成。
库的命名
库的搜索路径:从左到右搜索-L指定的目录。由环境变量指定的目录 (LIBRARY_PATH)。由系统指定的目录。
ls /user/include/...
ls /lib64/libc*...//查看C库。
使用时应说明:头文件路径、库文件路径、库文件名:gcc -t1.c -0 t1 -I . -L . -lmytest
; -I 指定头文件路径;-L 指定库路径;-l(小写L) 指定库名。静态链接:进程地址空间的代码区就包含了静态库的代码,用不着共享区。
//方法声明和实现
#pragma once//add.h
int add(int x ,int y);
#include "add.h"//add.c
int add(int a, int b)
{
return a + b;
}
----
#pragma once//sub.h
int sub(int a, int b);
#include "sub.h"//sub.c
int sub(int a, int b)
{
return a - b;
}
----//编译
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
----//库文件归档打包
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
----//查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o //t:列出静态库中的文件
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o //v:verbose 详细信息
----//使用时
#include
#include "add.h"
#include "sub.h"
int main( void )
{
int a = 10;
int b = 20;
printf("add(10, 20)=%d\n", a, b, add(a, b));
a = 100;
b = 20;
printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}
----//运行 main。c
[root@localhost linux]# gcc main.c -o main -I . -L . -lmymath /-l跟库名即可
C语言编译的时候,没用这么明显的选项,是因为:
库文件和头文件在默认路径下能找到;gcc编译C代码时,默认就应链接 libc;
自己的文件不加选项,需要将头文件和库文件拷贝到默认路径下—这也叫做库的安装。还可以更改内核级的配置文件。拷贝自己库文件或更改配置文件都容易污染官方的库文件。
gcc -shared -o libtest.so *.o
gcc -fPIC -c add.c
gcc 编译使用库时和静态库的使用方法相同…。但解决运行时路径问题,即编译器知道了头文件的位置、库文件的位置,但运行时系统加载器不知道 .so 动态库的路径。 ldd test
:发现 libtest.so not found。
解决的方法:
LD_LIBRARY_PATH
…还好ldconfig
更改配置文件/etc/ld.so.conf.d/,ldconfig更新(不推荐)---//makefile 动态库文件编译及合并的方法使用
libmymath.so:add.o sub.o
gcc -shared -o $@ $^
add.o:add.c
gcc -fPIC -c $^
sub.o:sub.c
gcc -fPIC -c $^
---//make...
----//引入库的使用
[saul@VM-12-7-centos tt730]$ echo $LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/saul/tt730/libtest.so
gcc test.c -o test -I . -L . -lmymath
---//不用make的制作使用过程
[saul@VM-12-7-centos tt730]$ vim add.h
[saul@VM-12-7-centos tt730]$ vim add.c
[saul@VM-12-7-centos tt730]$ ls
add.c add.h
[saul@VM-12-7-centos tt730]$ gcc -fPIC -c add.c //会自动生成。o
[saul@VM-12-7-centos tt730]$ ls
add.c add.h add.o
[saul@VM-12-7-centos tt730]$ gcc -shared -o libtest.so *.o
[saul@VM-12-7-centos tt730]$ ls
add.c add.h add.o libtest.so
[saul@VM-12-7-centos tt730]$ echo $LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/saul/tt730/libtest.so
[saul@VM-12-7-centos tt730]$ touch t1.c
[saul@VM-12-7-centos tt730]$ vim t1.c
#include "add.h"
#include
int main()
{
printf("%d\n",add(20,30));
return 0;
}
[saul@VM-12-7-centos tt730]$ gcc t1.c -o t1 -I . -L . -ltest
[saul@VM-12-7-centos tt730]$ ./t1
50
linux文件管理(inode、文件描述符表、文件表)
基础IO(下)——Linux
linux中文件索引节点知识