【Linux】基础IO介绍

文章目录

  • 1. 回顾C文件接口
    • 1.1 接口介绍
    • 1.2 示例
  • 2. 系统文件I/O
    • 2.1 接口介绍
    • 2.2示例
  • 3. open函数返回值
  • 4. 文件描述符fd
  • 5. 重定向
  • 6. FILE文件流指针
  • 7. 动态库和静态库
  • 8. 理解文件系统

1. 回顾C文件接口

1.1 接口介绍

打开文件的方式:

FILE *fopen( const char *path, const char *mode );
  1. path:需要打开的文件,需要加上文件的路径,可以是一个相对路径(相对于程序而言),也可以是一个绝对路径
  2. mode:以什么方式打开文件
  • r:以读模式打开文件,如果说文件不存在,则报错
  • r+:以读写模式打开文件,如果文件不存在,则报错
  • w:以写模式打开文件,如果 文件不存在则创建文件。如果文件存在,则清空文件内容(截断文件),文件流指针指向文件的头部
  • w+:以读写模式打开,如果文件不存在则创建文件。如果文件存在,则清空文件内容(截断文件),文件流指针指向文件的头部
  • a:以追加模式打开,如果文件不存在则创建;并不能对文件进行读,只能在文件的末尾进行追加写
  • a+:以追加模式打开,如果文件不存在则创建;支持读文件,在文件的末尾进行写
  1. 返回值:返回了一个文件流指针

读文件的方式:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  1. ptr:将读到的内容保存到ptr当中
  2. size:块的大小,单位是字节含义:表示一次读几个字节
  3. nmemb:块的个数,期望读到的块数
  • size *nmemb,单位是字节,用来衡量总共读到的字节数量
  • 注意:如果size不为1,块的大小不为1,则 能用返回值来讲读到了多少个字节
  • 常见的用法:将size置为1,每一个块的大小就是1;要从文件当中读多少字节,就设置多少块就可以了
  1. stream:文件流指针,从哪里读
  2. 返回值:返回成功读到的块的个数,实际读到的块数

写文件的方式:

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE*stream);
  1. ptr:要往文件当中写的内容
  2. size:块的大小,意味着一次写多少字节
  3. nmemb:块的个数
  • 判断写了多少字节= size * nmemb
  • 一般的用法,将size设置成为1,意味着块的个数,就相当于写多少字节
  1. stream:文件流指针,往哪里写
  2. 返回值:返回成功写入块的个数

偏移文件流指针的方式:

int fseek(FILE *stream, long offset, int whence)
  1. stream:文件流指针(要操作的文件流指针)
  2. offset.:偏移量,针对于whence而言的
  3. whence:偏移到哪里去
  • SEEK_SET:将文件流指针重置会文件的头部
  • SEEK_END:将文件流指针重置到文件的尾部
  • SEEK_CUR:将文件流指针重置到当前位置

关闭文件的方式:

int fclose(FILE* fp);

stdin & stdout & stderr
C默认会打开三个输入输出流,分别是stdin, stdout, stderr。
仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,文件指针。

1.2 示例

hello.c写文件:

#include 
#include 
int main()
{
	FILE *fp = fopen("myfile", "w");
	if (!fp){
		printf("fopen error!\n");
	}
	const char *msg = "hello bit!\n";
	int count = 5;
	while (count--){
		fwrite(msg, strlen(msg), 1, fp);
	}
	fclose(fp);// 关闭文件流指针
	return 0;
}

hello.c读文件:

#include 
#include 
int main()
{
	FILE *fp = fopen("myfile", "r");
	if (!fp){
		printf("fopen error!\n");
	}
	char buf[1024];
	const char *msg = "hello bit!\n";
	while (1){
		//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
		ssize_t s = fread(buf, 1, strlen(msg), fp);
		if (s > 0){
			buf[s] = 0;
			printf("%s", buf);
		}
		if (feof(fp)){
			break;
		}
	}
	fclose(fp);
	return 0;
}

2. 系统文件I/O

2.1 接口介绍

打开文件的方式:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  1. pathname: 要打开或创建的目标文件
  2. flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
  3. 参数:
    O_RDONLY: 只读打开
    O_WRONLY: 只写打开
    O_RDWR : 读,写打开
    这三个常量,必须指定一个且只能指定一个
    O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    O_APPEND: 追加写
  4. 返回值:
    成功:新打开的文件描述符
    失败:-1

写文件的方式:

ssize_t write(int fd,  const void* buf, size_t count)
  1. fd:文件描述符
  2. buf:写入的数据
  3. count:写入的数据大小(单位是字节)

读文件的方式:

ssize_t read(int fd, void *buf, size_t count)
  1. fd:文件描述符,从哪里进行读
  2. buf:读到哪里去(程序员在代码当中定义的缓冲区)
  3. count:最大可以读多少字节
    注意的点:一定要预留\0的位置

偏置文件流指针的方式:

off_t lseek(int fd, off_t offset, int whence)
  1. fd:需要操作的文件描述符
  2. offset:偏移量
  3. whence:偏移到哪里去
  • SEEK_SET:将文件流指针重置会文件的头部
  • SEEK_END:将文件流指针重置到文件的尾部
  • SEEK_CUR:将文件流指针重置到当前位置

关闭文件的方式:

int close(int fd)

2.2示例

hello.cpp 写文件:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
	umask(0);
	int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
	if (fd < 0){
		perror("open");
		return 1;
	}
	int count = 5;
	const char *msg = "hello bit!\n";
	int len = strlen(msg);
	while (count--){
		write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
	}
	close(fd);
	return 0;
}

hello.cpp 读文件:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
	int fd = open("myfile", O_RDONLY);
	if (fd < 0){
		perror("open");
		return 1;
	}
	const char *msg = "hello bit!\n";
	char buf[1024];
	while (1){
		ssize_t s = read(fd, buf, strlen(msg));//类比write
		if (s > 0){
			printf("%s", buf);
		}
		else{
			break;
		}
	}
	close(fd);
	return 0;
}

3. open函数返回值

  • fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)
  • open close read write lseek 都属于系统提供的接口,称之为系统调用接口
    【Linux】基础IO介绍_第1张图片
    系统调用接口和库函数的关系,一目了然。
    所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

4. 文件描述符fd

0 & 1 & 2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器 所以输入输出还可以采用如下方式:

#include 
#include 
#include 
#include 
#include 
int main()
{
	char buf[1024];
	ssize_t s = read(0, buf, sizeof(buf));
	if (s > 0){
		buf[s] = 0;
		write(1, buf, strlen(buf));
		write(2, buf, strlen(buf));
	}
	return 0;
}

【Linux】基础IO介绍_第2张图片

  1. 文件描述符就是内核当中维护的fd_array数组的下标,下标是从0开始的,所以文件描述符是一个正整数;
  2. 当程序员操作文件的时候,其实就是通过文件描述符,找到fd_array数组当中对应的元素,每一个元素都对应一个文件信息,内核通过操作数组元素对应的文件信息,找到对应的文件,从而实现文件操作。

文件描述符就是从0开始的小整数

  • 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。
  • 于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。
  • 每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!
  • 本质上,文件描述符就是该数组的下标。
  • 所以,只要拿着文件描述符,就可以找到对应的文件。

文件描述符的分配规则
在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。最小未占用规则,只要fd_array数组当中从小到大,哪个位置没有占用,则新打开的文件就会占用该位置。

5. 重定向

#include 
#include 
#include 
#include 
#include 
int main()
{
	close(1);
	int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
	if (fd < 0){
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	fflush(stdout);
	close(fd);
	exit(0);
}

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <。

使用 dup2 系统调用:
函数原型如下:

#include 
int dup2(int oldfd, int newfd);

oldfd和newfd都是文件描述符
eg:想要标准输出变成一个程序员打开的文件dup2(2,3);
注意事项:

  • newfd拷贝oldfd。
  • 如果oldfd是一个无效的文件描述符,则dup2什么事情都不干,newfd还是指向原来的文件。
  • 如果newfd和oldfd是同样的值,则dup2什么事情都不干。
  • 错误,返回小于0的值。

6. FILE文件流指针

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。

  • 所以C库当中的FILE结构体内部,必定封装了fd。
#include 
#include 

int main()
{
	const char *msg0 = "hello printf\n";
	const char *msg1 = "hello fwrite\n";
	const char *msg2 = "hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}

运行结果:
hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现printf 和fwrite (库函数)都输出了2次,而write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

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

综上: printf fwrite 库函数会自带缓冲区,而write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write 没有缓冲区,而printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
【Linux】基础IO介绍_第3张图片

7. 动态库和静态库

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

动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

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

生成静态库
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[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
ar是gnu归档工具,rc表示(replace and create)

查看静态库中的目录列表
[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 详细信息

[root@localhost linux]# gcc main.c -L. -lmymath
-L 指定库路径
-l 指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行。

库搜索路径

  1. 从左到右搜索-L指定的目录。
  2. 由环境变量指定的目录 (LIBRARY_PATH)
  3. 由系统指定的目录
    • /usr/lib
    • /usr/local/lib

生成动态库
shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
示例:
[root@localhost linux]# gcc -fPIC -c sub.c add.c
[root@localhost linux]# gcc -shared -o libmymath.so*.o
[root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o

使用动态库
编译选项
l:链接动态库,只要库名即可(去掉lib以及版本号)
L:链接库所在的路径.
示例: gcc main.o -o main –L. -lhello

运行动态库
1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib
2、更改LD_LIBRARY_PATH
3、ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新

使用外部库
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。

8. 理解文件系统

我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 “9月 13 14:56” a.out
-rw-r–r--. 1 root root 654 “9月 13 14:56” test.c

每行包含7列:

  • 模式
  • 硬链接数
  • 文件所有者
  • 大小
  • 最后修改时间
  • 文件名

stat命令能够看到更多信息
[root@localhost linux]# stat test.c
File: “test.c”
Size: 654 Blocks: 8 IO Block: 4096 普通文件
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

inode
为了能解释清楚inode我们先简单了解一下文件系统。
【Linux】基础IO介绍_第4张图片
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个操作:

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

硬链接:
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -1i
abc def 263466 abc 263466 def
创建:
In [源文件] [待创建 出来的硬链接文件名称]
注意:
源文件和硬链接文件两者具有同样的inode节点信息

软链接:
类比:类似于win环境当中创建快捷方式

263563 -rw-r–r--. 2 root root 0 9月 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9月 15 17:53 abc.s -> abc
263563 -rw-r–r--. 2 root root 0 9月 15 17:45 def

创建:In -s [源文件] [待创建出来的软链接文件]
“-” 代表普通文件,软链接文件–>,“l”代表软链接文件
注意:
1.创建出来的软链接文件,指向源文件,文件类型是“l”,不论是修改软链接文件还是修改源文件是一回事!
2.软链接文件和源文件具有不同的inode节点号,相当于 两个inode节点,都保存了文件在磁盘当中的存储位置
3.如果使用“ll”命令看到的软链接文件指向的源文件一直在闪烁,说明源文件不存在
4.警告!!如果要删除源文件,一定记着把软链接文件也一起删除掉

你可能感兴趣的:(Linux知识总结)