目录
本节重点:回顾库函数IO接口、学习linux下系统调用IO接口、理解文件描述符和文件流指针、重定向的原理、以及动态库和静态库的生成和使用。
1.库函数IO接口
fopen,fwrite,fread,fseek,fclose
1.1 fopen
- FILE *fopen(char *pathname,char *mode)
- pathname:文件路径名;mode:文件的打开方式
- r:只读方式打开文件,文件必须存在
- r+:读写方式打开文件,文件必须存在
- w:只写方式打开文件,文件不存在则自动创建,存在则清空内容
- w+:读写方式打开文件,文件不存在则自动创建,存在则清空内容
- a:追加写方式打开文件,文件不存在则自动创建,存在则写入数据总是写入文件末尾
- a+:读+追加写方式打开文件,文件不存在则自动创建。存在则写入数据总是写入文件末尾
- b:二进制方式打开文件(在系统调用中IO接口并不取反文本和二进制数据,统一按照二进制处理。但是库函数不一样,区分文本和二进制,默认文本操作,但是有点文本一个字符占据多个字节。例如:一个文件中有10个字节数据,但是读取的时候读取完毕后读取大小是9,这个9表示的不是9个字节而是9个字符。
) - 返回值:打开文件成功则返回一个FILE*文件流指针作为文件的操作句柄,失败返回NULL 。
1.2 fwrite
- size_t fwrite(const void* data, size_t bsize, size_t nmemb, size_t nmemb, FILE*fp)
- data:要写入文件的数据的所在空间首地址
- bsize:块大小;
- bsize* nmemb:实际要写入文件的大小
- fp:fopen返回的文件流指针,表示要操作哪一个文件
- 返回值:成功返回实际完整操作的块个数;失败返回0
- 注意:r+/w/w+ 方式打开文件之后, 文件的默认读写位置在文件起始,如果文件本身有数据,就会覆盖,并且读写位置会随着数据的写入向后偏移;a/a+方式打开文件之后,读默认是从文件起始读的,但是如果写会使读写位置跳转到文件末尾,将数据追加到末尾。
1.3 fread
- size_t fread(void *buf, size_t bsizew, size_t nmemb, FILE *fp)
- buf:一块空间的首地址,用于存放从文件中读取的数据
- bsize:块大小;
- nmemb:块个数;
- bsize*nmemb:就是实际要读取的数据的大小
- fp:文件流指针
- 返回值:成功返回实际操作的完整块个数,读取到文件末尾了返回0,出错了返回0。
- 注意:这个函数返回值存在多义性,返回0的时候无法直接确定是出错了还是读取到文件末尾了。
例如:一个文件100字节,我们现在读取数据块大小200,块个数1,因为没有读取到完整的一块,所以我们不能确定是出错了还是正常的。
因此,建议fread读取数据的时候块大小设置为1,想要读取的长度设置为块个数,这样返回值就能告诉我们读取了多少数据。
1.4 fseek
改变文件读写位置,跳转到哪里就从哪里开始读写。
- int fseek(FILE *fp, int offset,int whence)
- fp:文件流指针
- offset:偏移量
- whence:偏移起始位置
- SEEK_SET-起始/SEEK_CUR-当前/SEEK-END-末尾
- 返回值:成功返回0;返回失败-1
1.5 fclose
int fclose(FILE *fp)
关闭文件释放资源。
2. 系统调用IO接口
open ,read,weite,lseek,close
2.1 open
- int open(char *pathname,int flag,mode_t mode)
- pathname:要打开的文件路径名
- flag:文件打开标志——决定了文件的打开方式
必选标志:O_RDONLY-只读/ O_WRONLY-只写 /O_RDWR-读写
可选标志:O_APPEND-追加写 | O_CREAT-文件不存在则创建 | O_TRUNC-文件不存在则截断为0 | O_EXCL-文件存在则报错
w+ :读写+创建+截断 O_RDWR | O_CREAT |O_TRUNC
a+ :读+追加写,创建 O_RDWR | O_APPEND |O_CREAT - mode:文件的权限,通常采用八进制数字设定 0777,如果使用了O_CREAT就一定要有第三个参数。
文件的创建权限是受到umask掩码影响的。实际文件得到的权限: mode&(umask) - 返回值:成功返回一个非负整数-文件描述符-是文件的操作句柄;失败返回-1
- mode_t umask(mode_t mask); 通常在程序 起始阶段调用 umask(0)将当前进程的创建码设置为0。
2.2 read
- size_t read(int fd, void *buf, int len)
- fd:文件描述符-操作句柄;
- data:要写入文件数据的空间首地址;
- len:想要写入文件的数据长度
- 返回值:成功返回实际写入文件的长度,失败返回-1;
- ssize_t read(int fd, void *buf,int len);
- fd:open返回的文件描述符-操作句柄-表示操作哪个文件
- buf:一块空间地址,用于存放读取到的数据
- len:要读取的数据长度
- 返回值:成功返回实际读取到的数据长度;失败返回-1
2.3 write
头文件是#include<unistd.h>
- ssize_t write(int fd, void *data, int len )
- fd:文件描述符-操作句柄
- data:要写入文件的数据的空间首地址
- len:想要写入文件的数据长度
- 返回值:成功返回实际写入文件的数据长度;失败返回-1
2.4 lseek
- off_t lseek(int fd, off_t offset, int whence)
- fd:文件描述符
- offset:偏移量
- whence:偏移量起始位置 SEEK_SET/SEEK_CUR/SEEK_END
- 返回值:成功返回跳转后的位置相对于文件起始位置的长度(额外用法:跳到文件末尾则返回文件长度);返回失败返回去-1
2.5 close
- int close(int fd):关闭文件释放资源
2.6 例子
-
问题描述
使用代码打开当前路径下的“bite”文件(如果文件不存在在创建文件),向文件当中写入“i like linux!”. 在从文件当中读出文件当中的内容, 打印到标准输出当中; 关闭文件描述符。 -
代码截图
-
结果截图:
3. 文件描述符与文件流指针
3.1 文件描述符
文件描述符是open返回的一个非负整数
- 文件描述符本质是什么?如何标识操作的文件?
文件描述符的本质:内核中,进程打开文件IO信息指针数组的一个下标。
当我们通过描述符操作文件的时候,在pcb中找到files_ struct结构体指针,进而找到结构,找到结构体中的数组,以描述符作为下标获取到文件描述信息的地址,通过描述信息操作文件
3.2 文件流指针
注意:
- 类型不同,不能混用,文件流指针是struct FILE *
;文件描述符是int 。 - 文件流指针是库函数的操作句柄,文件描述符是系统调用接口的句柄。
- 库函数和系统调用接口是一层上下级调用的关系:库函数中封装的就是系统调用接口。
- eg:fwrite (用的是文件流指针)—> write (用的是文件描述符)。
文件流指针中不仅有文件描述符,还有我们通常所说的缓冲区。
也就是说,缓冲区这个东西不是系统调用中定义的,而是大佬们在上层封装的时候在流指针中封装的。
这个缓冲区所占空间(用户空间)—将这个缓冲区称之为用户态缓冲区。
这也是为什么exit系统调用接口退出进程前不会刷新缓冲区的原因
4. 重定向
改变数据的流向,让原本写入a文件的数据,不再写入a文件,而是写入b文件。
-
一个进程运行起来默认会打开三个文件:
标准输入,标准输出,标准错误,这三个文件占据的描述符就是 0-标准输入, 1-标准输出,2-标准错误。
-
重定向原理:改变文件描述符这个下标所对应的文件描述信息指针(操作的描述符 没有改变,但是改变了操作的文件)
4.1 重定向举例
-
编写一个redirect.c文件,编译一下,这里我们的write函数的第一个参数是1,所以就是打印到标准输出(即屏幕上)
-
修改redirect.c文件内容。这时候运行代码。就没有输出,而是输出到了我们打开的文件里面~
- ls>>text.txt 实现原理:
ls 是一个指令程序,运行时会启动进程,进程由 shell 创建,shell 创建了 ls 子进程之后只需要将这个进程的 1 号描述符对应的文件信息指针改变就可以实现在不改变 ls 程序的情况下改变数据的流向。
注意: 程序替换,并不会初始化文件描述信息指针数字。因此提前修改了1号描述符的信息之后,程序替换,向1中写入数据就会把数据写入指定的文件
4.2 改进版的minishell
- 先介绍一下——dup2 系统调用
- 函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd); - 让 newfd 赋值 oldfd 对应的描述信息,成功则关闭newfd原来指向
- 说白了dup2这个接口就是让两个描述符都指向oldfd所指向的文件,相当于是对newfd进行了重定向
- 在我们自己的minishell中添加重定向功能的实现:
“>>” : 追加重定向
“>”: 清空重定向
重定向实现:在创建了子进程,程序替换之前,将标准输出给重定向到指定的文件上dup2(fd, 1)
追加:打开fd的文件时使用O_APPEND。清空: 打开fd文件时使用O_ TRUNC。
Is -l >> test.txt
(1)捕捉输入;
(2)解析是否包含重定向(将命令截断得到命令+重定向类型+重定向文件名) ;
(3)解析指令(得到 命令名称,运行参数)
(4)创建子进程;
(5)子进程中先按照指定重定向类型打开重定向文件;
(6)对子进程进行标准输出重定向,让标准输出1指向打开的文件;
(7)程序替换让子进程运行对应功能的shell指令程序, 比如s;
(7)等待子进程退出;
改进的minishell的代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
int main()
{
while(1) {
printf("[user@host ~]$ ");//因为没有换行,所以数据没有刷新缓冲区,无法打印出来
fflush(stdout);//刷新标准输出缓冲区
char cmd[128] = {0};
fgets(cmd, 127, stdin);//从标准输入获取一行数据,如果一行过长,就只获取127个字节
cmd[strlen(cmd) - 1] = '\0';//将字符串末尾的换行,替换成结尾标志
//解析字符串 [ ls -l -a ]
char argv[32][32] = {{0}};
int argc = 0, count = 0;
char *ptr = cmd;
while(*ptr != '\0') {
if (*ptr != ' ' && *ptr != '\0') {
count = 0;
while(*ptr != ' ' && *ptr != '\0') {
argv[argc][count] = *ptr;
count++;
ptr++;
}
argc++;
}
ptr++;
}
//for (int i = 0; i < argc; i++) {
// printf("argv[%d] = [%s]\n", i, argv[i]);
//}
char *myargv[32] = {NULL};
for (int i = 0; i < argc; i++) {
myargv[i] = argv[i];
}
pid_t pid = fork();
if (pid == 0) {
//execvp(char *filename, char *argv[]);
int ret = execvp(argv[0], myargv);
if (ret < 0) {
perror("execvp error:");
exit(0);
}
}
//能走下来的只有父进程
wait(NULL);
}
return 0;
}
5. 动态库和静态库
5.1 静态库与动态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
5.2 动态库和静态库的生成
- 库:很多已经写好的功能代码,所打包而成的一个文件(库中不能有main函数)
- 库的命名: lib 作为前缀,中间名称,最后 .so 作为动态库后缀,.a 作为静态库后缀
-
vi 三个文件
现将所有的 .c 编译汇编完成后会生成自己的 .o 二进制指令文件
gcc -fPIC -c $^ -o $@
-
将所有的 .o 二进制指令文件打包到一起(要生成的不是可执行程序而是库)
gcc --shared $^ -o lib**.so
(1) 生成一个动态库:gcc --shared $^ -o lib**.so
(2)生成一个静态库:ar -cr lib**.a **.o **.o
5.3 动态库和静态库的使用
- 生成可执行程序的时候使用:
(1)使用 -l 来指定要链接是库的名称(前提是库文件需要放到指定路径下:/usr/lib64/)
(2)如果设置环境变量,将库文件所在目录加入到环境变量路径中:export
LIBRARY_PATH=${LIBRARY_PATH}:./ (注意这里没有空格)
(3)使用gcc -L 选项指定库文件的所在路径(常用于链接静态库——因为静态库没有依赖,运行时不需要加载)
gcc main.c -o main -L./-lchild(注意./-lchild中间没有空格)
- 运行可执行程序的时候使用:(仅限于动态库——因为只有链接动态库,运行程序的时候才需要加载)静态库使用时是把库中用到的代码直接放入到可执行程序,动态库使用时只记录函数符号信息,因此运行时依赖。
- (1)运行时设置环境变量中的库文件加载路径(与上面的 export
LIBRARY_PATH= L I B R A R Y P A T H : . / 是 成 对 使 用 的 ) e x p o r t ∗ ∗ L D L I B R A R Y P A T H ∗ ∗ = {LIBRARY_PATH}:./ 是成对使用的) export **LD_LIBRARY_PATH**= LIBRARYPATH:./是成对使用的)export∗∗LDLIBRARYPATH∗∗={LD_LIBRARY_PATH}:./
- 一个目录下既有动态库也有静态库,同名,gcc生成可执行程序时默认优先链接动态库(gcc默认的链接方式就是动态链接(链接动态库))