Linux文件

文件操作

关于C中的文件操作,详见C文件操作
除了C语言中的文件接口,其他各种语言也都提供了接口,在Linux下,也提供了几个系统调用接口来进行文件操作…这里只对常用的接口进行介绍

打开文件

#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_CREAT:若文件不存在,则创建。需要使用mode选项,来指明新文件的访问权限
        O_APPEND:追加写
        O_NONBLOCK:非阻塞状态
返回值:
        若成功,返回新的文件描述符
        若失败,返回-1

关闭文件

#include <unistd.h>
int close(int fd);

参数:
fd:文件描述符
返回值:
成功返回0,失败返回-1,并产生错误码

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

参数:
      fd:文件描述符
      buf:读缓冲区,用来接收read函数读取到的内容
      count:读取大小,单位字节
返回值:
      成功返回0, 失败返回-1

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

参数:
      fd:文件描述符
      buf:写缓冲区,用来存放要往文件中写入的内容
      count:写大小,单位字节

lseek

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

作用:在指定的文件描述符中将文件指针定位到相应位置
参数:
      fd:文件描述符
      offset:偏移量,相对于第三个参数whence的偏移量
      whence:文件指针的固定其实偏移位置,与第二个参数offset配合使用,有三个常量选项:
          SEEK_SET:文件开头
          SEEK_CUR:当前文件指针位置
          SEEK_END:文件末尾

返回值:
      成功则返回从当前文件开头至当前位置的以字节为单位的偏移量,失败则返回-1

文件描述符fd

Linux进程默认情况下会有三个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
0、1、2对应的物理设备一般是:键盘、显示器、显示器
在这里插入图片描述
如上图所示,文件描述符就是从0开始的小整数。当打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,就用files_struct结构体表示一个已经打开的文件对象。每个进程都有一个指针*files,指向一张表files_struct,该表最终要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标,只要拿着文件描述符,就能找到对应的文件

文件描述符的分配规则

最小未占用原则:每次打开一个文件时,在files_struct数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
有如下例子可进行验证:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("myfile", O_RDONLY);
	if(fd < 0)
	{
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	
	close(fd);
	return 0;
}
//输出结果
//fd: 3
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	close(0);  //关闭标准输入
	//close(2);  //关闭标准错误
	int fd = open("myfile", O_RDONLY);
	if(fd < 0)
	{
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);

	close(fd);
	return 0;
}
//输出结果:
//fd: 0

注意

1、每次打开一个文件,在使用完 后,需要关闭文件,否则会造成文件句柄泄漏,可能会导致不能成功打开文件
2、可以用**“ll /proc/[pid]/fd/”**查看文件句柄是否存在泄漏
3、每个进程打开的文件句柄都有上限,可用ulimit -a查看或设置(open_files)
在这里插入图片描述

文件描述符和文件流指针区别

在上面的图中已经可以清晰的看出:files_struct的下标就是文件描述符,files_struct中的元素的值就是指向FILE* 结构体类型的指针
详解如下(对照上图):

文件描述符:

在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,即已打开的文件在内核中用 File 结构体表示,文件描述符表中的指针指向 File 结构体
优点是兼容POSIX标准,许多系统调用都依赖于它
缺点是不能移植到unix之外的系统上去。

文件流指针:

C语言中使用文件指针而不是文件描述符做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构主要包括一个I/O缓冲区和一个文件描述符。而文件描述符是文件描述符表中的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。文件指针的优点是C语言中的通用格式,便于移植。

【注】

既然FILE结构中含有文件描述符,那么可以使用fopen来获得文件指针,然后从文件指针获取文件描述符,文件描述符是唯一的,而文件指针却不是唯一的,但指向的对象是唯一的

重定向

概念:将文件描述符重新绑定到新的文件上

使用

命令行

>
示例:echo “hello world!” > test.txt
解释:将标准输出文件描述符重新绑定到test.txt文件,即将hello world!写入test.txt中,test.txt中原有内容清空
>>
示例:echo “hello world!” >> test.txt
解释:同上,但是写入文件时,不会清空原有文件中的内容,只是会在文件末尾进行追加写入

函数

# include <unistd.h>
int dup2 (int oldfd, int newfd );
参数:
oldfd:原文件描述符                newfd:新文件描述符
返回值:成功返回新的文件描述符,失败返回-1

使用举例:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
	int fd = open("./test", O_CREAT | O_RDWR);
	if(fd < 0)
	{
		perror("open");
		return 1;
	}
	close(1);
	dup2(fd, 1);
	for(;;)
	{
		char buf[1024] = {0};
		ssize_t read_size = read(0, buf, sizeof(buf) - 1);
		if(read_size < 0)
		{
			perror("read");
			return 1;
		}
		printf("%s", buf);
		fflush(stdout);
	} 
	return 0;
}

FILE

探究如下代码:

#include <stdio.h>
#include <string.h>

int main()
{
	const char *msg0="printf\n";
	const char *msg1="fwrite\n";
	const char *msg2="write\n";

	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	
	fork();
	return 0;
}
//运行结果:
//printf
//fwrite
//write
//重定向至文件中 ./test > test.txt
//write
//printf
//fwrite
//printf
//fwrite

分析结果:printf、fwrite(库函数)都输出了两次,而write(系统调用)只输出了一次,解释如下:

C标准库函数写入文件时采用全缓冲方式,而写入显示器是行缓冲方式
库函数自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式有行缓冲变为全缓冲。所以放在缓冲区中的数据,就不会立即被刷新,甚至fork之后,但是进程退出后,会统一刷新,写入文件中
fork时,父子数据会发生写时拷贝,当父进程准备刷新的时候,子进程也就有了同样的数据,随机产生两份数据
write没有发生变化,说明没有所谓的缓冲

综上:
1、printf和fwrite库函数都会自带缓冲区,而write系统调用没有带缓冲区
2、这里的缓冲区,指的都是用户级缓冲区,其实为了提升整体性能,操作系统也会提供相关内核级缓冲区
3、printf和fwrite是库函数,write是系统调用,库函数是系统调用的“上层接口”,是对系统调用所谓的封装,但是write没有所谓的缓冲区,所以说明,该缓冲区是在封装的时候加上的,所以由C标准库提供

C标准库中对于FILE结构体定义如下图所示:

typedef struct _IO_FILE FILE;

在这里插入图片描述
对应到上面进程的文件结构图中,大体如下:
在这里插入图片描述

动态库和静态库

动态库(.so)

概念:程序在运行的时候采取链接动态库的代码,多个程序共享使用库的代码
1、一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
2、在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存,这个过程称为动态链接
3、动态库可以在多个程序间共享,所以动态链接是的可执行程序更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间

生成

-shared:产生一个动态库的命令
-fPIC:产生位置无关的代码----相当于so未见当中的函数地址都是一个逻辑地址
举例:gcc -shared -fPIC test.c -o libtest.so

使用

-L:指定动态库的路径在哪里
-l:指定库名,链接动态库,只要库名即可(去掉lib以及版本号)
LD_LABRARY_PATH:搜索动态库的环境变量
举例:gcc main.c -o main -L. -ltest

静态库(.a)

程序在编译链接的时候把库的代码链接 到可执行文件中,程序运行的时候将不再需要静态库

生成

ar -rc lib[filename].a [obj文件,即.o文件]
ar是GNU归档工具,rc表示(replace and create)

查看

ar -tv lib[filename].a
t : 列出静态库中的文件
v : verbose详细信息

使用

gcc [源码文件] -o [生成的可执行程序] -L [path] -l (静态库名称)
-L:指定库路径
-l:指定库名
示例:gcc main.c -o -L. -ltest

浅析文件系统

本篇以Linux下的EXT2文件系统为例:
在命令行使用stat + [filename]可以查看如下文件信息
在这里插入图片描述
那么什么是inode节点?先来解释一下文件系统
在这里插入图片描述
Linux EXT2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被 划分为一个个的block,一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设 定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相 同的结构组成。政府管理各区的例子
  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没 有被占用
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。 i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等 数据区:存放文件内容

将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?
在这里插入图片描述
创建一个新文件主要有一下4个操作:

  • 存储属性
    内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
  • 存储数据
    该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
  • 记录分配情况
    文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  • 添加文件名到目录
    新的文件名abc,内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来

存储数据的过程:
需要在BlockBitmap当中去查找空闲的块,将带存储的文件分成不同的块存储在Data Blocks区域中,此时就需要inode节点来描述文件信息,所以就会在inodeBitmap区域去查找空闲的inode节点在inode节点中去填充文件信息,将inode放到inodeTable区域,将文件名称和inode节点当做目录项存储起来

查找数据的过程:
首先在inodeTable的目录项中根据文件名称找到inode节点根据inode节点当中的信息找到对应的块信息,将信息合并起来,就是文件的数据

软硬链接文件

目的:都是为了找到源文件

硬链接

ln [源文件] [要创建出来的硬链接文件]

硬链接文件并没有自己的inode节点,拥有的只是源文件的inode节点

在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应 的磁盘释放。
在这里插入图片描述

软链接

ln -s [源文件] [要创建出来的软链接文件]

软链接有自己的inode节点,软链接是通过名字引用另外一个文件

在这里插入图片描述


THE END

posted on 2020-01-14 10:13  createwell  阅读(235)  评论(0编辑  收藏  举报