【Linux】第五章-基础IO

第五章 基础IO

回顾标准库IO接口

  我们在C语言中已经见过很多包含在标准库中的IO接口:fopen fclose fwrite fread fseek...,在fopen中有一些不同的文件打开模式:r r+ w w+ a...,而要想对文件进行操作我们需要用到句柄——FILE*,之前我们称之为句柄,但它实际上是一个文件流指针,stdout stdin stderr他们也是文件流指针,用来操作和控制文件和IO流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//标准IO回顾
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
FILE* fp = NULL;//句柄,文件流指针
fp = fopen("./tmp.txt", "a");
if(fp == NULL)
{
perror("fopen error");
}
char* ptr = "Misaki";
fwrite(ptr, strlen(ptr), 1, fp);
char buf[1024] = { 0 };
fread(buf, 1023, 1, fp);
printf("buf = %s\n", buf);
return 0;
}

系统调用接口

  除过标准库中的IO接口我们的系统中也有系统调用接口可以直接进行IO流的控制,我们自然可以用这些系统调用接口来控制文件。标准库中的IO接口也不过是系统调用接口的一层封装。而直接使用系统调用接口可以实现的功能更为丰富,但同时也更难以操作。

open()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
//int open(const char *pathname, int flags, mode_t mode);
//pathname:路径名
//flags:选项标志
// 必选:必选其一
// O_RDONLY 只读
// O_WRONLY 只写
// O_RDWR 可读可写
// 可选项:
// O_CREAT 文件不存在则创建,存在则打开
// O_EXCL 与O_CREAT同用时若文件存在则报错
// O_TRUNC 打开文件同时截断文件长度为0
// O_APPEND 写追加模式
//mode:创建文件时给定权限(八进制数字)
// mode & (umask)
//返回值:文件描述符-正整数 错误:-1
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}

  此时由于我们加上了截断选项文件中的内容会被全部清空。如果我们在语句中不加mode权限系统会默认给随机的权限,这是十分危险的,我们自己创建的文件可能连我们自己都打不开。

  我们可以看到我们创建的文件以及权限已经按照我们的预期设置好了。

open

write()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
//ssize_t write(int fd, const void *buf, size_t count);
//fd:打开文件所返回的文件描述符
//buf:要想文件写入的数据
//count:要写的数据长度
//返回值:实际的写入字节数,错误:-1
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
}

  这样我们即可将数据写入到文件中。之后是读取函数。

read()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
//ssize_t read(int fd, void *buf, size_t count);
//fd:打开文件所返回的文件描述符
//buf:对读取到的数据进行存储的位置
//count:要读取得数据长度
//返回值:实际的读取的字节数,错误:-1
memset(buf, 0x00, 1024);//将缓冲区清零
ret = read(fd, buf, 1023);
if(ret < 0)
{
perror("read error");
return -1;
}
printf("read buf = %s\n", buf);
printf("ret = %d\n", ret);
}

[misaki@localhost 第五章-基础IO]$ ./SystemIO
read buf =
ret = 0

lseek()

  但是我们发现并没有向缓冲区中读取文件中的数据,这是因为我们在写入数据后文件中的描述符停留在文件末尾,之后就是文件结束符因此无法读取之前的数据,于是我们需要一个函数将描述符移动到文件最开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
//off_t lseek(int fd, off_t offset, int whence);
//fd:打开文件所返回的文件描述符
//offset:偏移量
//whence:偏移位置
// SEEK_CUR 当前位置
// SEEK_SET 文件起始位置
// SEEK_END 文件末尾
lseek(fd, 0, SEEK_SET);
memset(buf, 0x00, 1024);//将缓冲区清零
ret = read(fd, buf, 1023);
if(ret < 0)
{
perror("read error");
return -1;
}
printf("read buf = %s\n", buf);
printf("ret = %d\n", ret);
}


[misaki@localhost 第五章-基础IO]$ ./SystemIO
read buf = Misaki
ret = 1023

  这样读取就成功了。

close

  使用完文件一定别忘了关闭,否则会造成文件描述符泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
lseek(fd, 0, SEEK_SET);
memset(buf, 0x00, 1024);//将缓冲区清零
ret = read(fd, buf, 1023);
if(ret < 0)
{
perror("read error");
return -1;
}
printf("read buf = %s\n", buf);
printf("ret = %d\n", ret);
//int close(int fd);
close(fd);
}

文件描述符

文件描述符?文件流指针?

  我们知道了标准库使用的是文件流指针,而系统调用接口使用的是文件描述符,那么什么是文件描述符呢?我们通过对系统调用接口的练习使用知道了文件描述符是一个整数。而我们知道库函数与系统调用接口是上下级调用关系,那么文件描述符在其中又起到什么作用呢?

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

文件描述符

  这张表存储在PCB中。

  文件流指针是一个结构体,其中就包括了文件描述符,当我们使用标准库接口进行io,则最终是通过文件流指针找到文件描述符进而对文件进行操作。

  而每个进程在执行时会默认打开三个文件stdin(标准输入) stdout(标准输出) stderr(标准错误)对应的物理设备一般是键盘,显示器,显示器,这三个默认打开的文件对应的文件描述符分别是0, 1, 2

缓冲区

  缓冲区实际上是文件流指针为每个文件所维护的一个缓冲区(用户态)。其保存在文件流指针中,但是如果我们不使用标准库IO而直接使用系统调用接口我们则不会使用到文件流指针,自然系统就不会创建缓冲区,更不会先将数据搁在缓冲区中。

        write(1, "Misaki", 7);

  这段命令会直接在标准输出文件中打印数据,并不会通过缓冲区。

文件描述符分配规则

  最小未使用。文件描述符默认会给文件分配当前最小的且未使用的文件描述符。

1
2
3
4
5
6
7
8
9
10
11
12
//文件描述符分配原则:最小未使用
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
close(1);//我们关闭了标准输出,文件描述符1空闲
int fd = open("./tmp.txt", O_RDWR);//tmp.txt的文件描述符被分配为最小的未使用1
printf("%d\n", fd);
fflush(stdout);//刷新缓冲区
close(fd);
}

  这段代码执行后在屏幕上不会有打印,但在tmp.txt文件中会打印1。这是因为标准输出已经被我们关闭了,并且文件描述符1此时已经指向了tmp.txt因此我们使用printf()进行打印也会打印在tmp.txt中。

重定向

  我们之前关闭了stdout文件并且让新创建的文件的文件描述符自动变成了1,由此我们原本要打印到屏幕上的内容到了tmp.txt中,这个过程我们叫输出重定向,及将原本要输入到一个文件中的内容重定向使其输入到另一个文件中。我们可以用> >>来进行重定向,同时我们也可以在程序中利用dup()系统调用来达到重定向的目的。

  dup(int oldfd, int newfd),将newfd重定向到oldfd所指向的文件,如果newfd已有打开文件则关闭文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*文件描述符*/                                           
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//int dup2(int oldfd, int newfd);
// 将newfd重定向到oldfd所指向的文件
// 若newfd本身已有打开文件,重定向时则关闭已打开文件
int fd = open("./tmp.txt", O_RDWR);

//让1也指向了fd所指向的文件
dup2(fd, 1);

printf("%d\n", fd);
fflush(stdout);
close(fd);
return 0;
}

文件系统

文件系统简介

  我们磁盘上没有一个分区就有一个文件系统用于管理文件存储。我们的文件系统是通过分块来存储文件数据的,为了方便的找到文件相关信息,我们文件系统创建了一个个inode结点用于存放一个个结点的文件信息,并且在inode中存放着文件数据具体存放的数据块的位置。并且还有一个inode table用来统一管理当前分区中所有的inode结点。同时我们还有两个工具inode_bitmapdata_bitmap用来分别在inode table数据区找到空闲位置用来存储文件的inode和文件的数据。
  存储文件流程:通过inode_bitmapinode table中找到空闲的inode结点,通过data_bitmap在数据区域找到空闲数据块,将数据块位置信息,记录到inode结点中,将文件数据写入数据块中,将文件名和inode节点号写入父目录文件中。父目录文件是目录下用于查看当前目录下有什么文件的表。如果我们要查询某个目录下的某个文件我们会现在目录文件中查询这个文件的inode结点信息(但其实这个结点是在inode_table中进行存储的),然后通过inode再找到这个文件的数据具体所存进的数据块,来查询数据。

硬链接/软链接

  硬链接是通过复制原文件的inode达到创建链接的目的,可以理解硬链接为原文件的别名。ln 原文件 硬链接,与原文件没有什么区别。

  软连接一个独立的文件,文件内容里存储着原文件的地址与名字,软连接有着新的inode结点,每次软连接通过它自身的数据找到原文件的文件名再查找到源文件。ln -s 源文件 软连接,是原文件的一个快捷方式。

  区别:

  1、删除原文件,软连接失效,硬链接无影响。

  2、软链接可以跨分区建立,硬链接不行。

  3、软连接文件可以针对目录创建,硬链接不行。

动态库/静态库

生成

  生成动态库:gcc -fPIC -c b.c -o b.o,gcc --share b.o -o libmytest.so

  生成静态库:gcc -c b.c -o b.o,ar -cr libmytest.a b.o

  gcc选项:-fPIC:产生位置无关代码。--share:生成一个共享库。

  ar选项:-c:创建一个静态库。-r:新增库文件。

库的使用

动态库

  由于我们动态库的默认查找路径是/lib64,我们可以将我们的库移到这个路径下就可以直接链接了,当然也可以使用一下这种方法来增加环境变量或者参数来让程序连接到库。

  1、我们要增加参数更改查找库的路径gcc a.c -o main -L. -lmytest。其中-L指定库的路径,-l指定库的名称。我们也可以增加环境变量来增加查找库的路径export LIBRARY_PATH=.

  2、我们的动态库在程序运行时也需要库的存在,因此我们要增加环境变量来让程序运行时也能找的到库。export LD_LIBRARY_PATH=.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add.h:
int add(int num1, int num2);
add.c:
#include "add.h"

int add(int num1, int num2)
{
return num1 + num2;
}
main.c:
#include <stdio.h>
#include "add.h"
int main()
{
printf("%d\n", add(1,3));
}

  终端中执行代码:

1
2
3
4
5
6
[misaki@localhost add]$ gcc add.c -fPIC -c -o add.o
[misaki@localhost add]$ gcc --share add.o -o libmyMath.so
[misaki@localhost add]$ export LD_LIBRARY_PATH=.
[misaki@localhost add]$ gcc main.c -L. -lmyMath -o main
[misaki@localhost add]$ ./main
4

静态库

  通常链接静态库的时候并不会使用-static静态链接,而是将静态库放到指定路径下直接使用gcc -L路径 -l库名称直接指定库的链接路径去生成可执行程序。-static会将可执行程序全部使用静态链接生成,不依赖任何动态库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add.h:
int add(int num1, int num2);
add.c:
#include "add.h"

int add(int num1, int num2)
{
return num1 + num2;
}
main.c:
#include <stdio.h>
#include "add.h"
int main()
{
printf("%d\n", add(1,3));
}

  终端中执行代码:

1
2
3
4
5
[misaki@localhost add]$ gcc -c add.c -o add.o
[misaki@localhost add]$ ar -cr libmyMath.a add.o
[misaki@localhost add]$ gcc main.c -L. -lmyMath -o main
[misaki@localhost add]$ ./main
4

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助