【Linux】基础IO——库函数与系统调用的关系
目录
- 引言
- 1.文件操作语言方案(C的文件操作接口-库函数)
- 打开文件、关闭文件——fopen、fclose
- 打开文件的模式
- 写入——fput、printf
- 读取——fgets
- 2.文件操作系统方案(系统的文件操作接口-系统调用)
- 打开文件、关闭文件——open、close
- open的参数flags的理解
- open的参数flag的学习
- 写入——write
- 读取——read
- 几个系统调用的使用
- 3.库函数与系统调用的关系
引言
打开文件的本质,就是将该文件的属性加载到内存中,OS内部存在大量被打开的文件,所以就需要管理这些文件:
每打开一个文件,都要在OS内创建该文件对象的struct结构体,然后将这些struct file用某种数据结构链接起来,所以在OS内部对文件的管理就转化成了对链表的增删查改。
文件可以分为两大类:
- 磁盘文件:没有被打开,存储在磁盘上
- 被打开的文件:被打开了,在内存中创建了对应的数据结构管理文件的属性
文件是被OS打开的,谁让OS打开?
——进程,所以我们所学习的都是 进程 与 被打开文件 的关系(struct task_struct 与 struct file);
1.文件操作语言方案(C的文件操作接口-库函数)
要对文件进行操作,首先要打开文件,打开操作完成后也要关闭文件,下面复习两个接口,fopen与fclose;
打开文件、关闭文件——fopen、fclose
打开文件——fopen
头文件:stdio.h
函数:FILE *fopen(const char *path, const char *mode);
参数:
path——打开文件的路径;
mode——打开文件的模式;
返回值:成功返回FILE的指针,失败返回NULL;
若失败会设置错误码errno(表明出错的原因),可以通过perror接收错误码,将错误码转化成错误码描述打印错误原因,也可以通过strerror将错误码转化成错误码字符串;
关闭文件——fclose
头文件:stdio.h
函数:int fclose(FILE *fp);
参数:pf:指向被打开的文件
返回值:成功返回0,失败返回EOF,并设置错误码errno。
打开文件的模式
打开模式 | 含义 | 若指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 创建新文件 |
“a”(追加) | 向文本文件尾添加数据 | 创建新文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 创建新文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立一个新的文件 | 创建新文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 创建新文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 创建新文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 创建新文件 |
解析:
如果fopen以w模式打开文件,写入的规则是:
1.首先将文件清空
2.从重头重新写入
(若只打开文件,不写入就关闭,文件也会被清空)
如果fopen以a模式打开文件,写入的规则是:
不会清空文件,每次写入都是从文件的结尾写入。
写入——fput、printf
1.fputs
打开文件后还需要进行写入操作,写入操作接口有很多,这里复习fputs。
写入——fputs
头文件:stdio.h
函数:int fputs(const char *s, FILE *stream);
参数:将字符串s写入到文件流stream当中。
返回值:成功返回非负数(写入了多少个字符),错误返回EOF;
以一段最简单的代码为例:在当前目录下创建log.txt文件,并写入5条hello world!。
//fputs文件写入
#include <stdio.h>
#define LOG "log.txt"
int main()
{
FILE *fp = fopen(LOG, "w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
const char *str = "hello world!\n";
int cnt = 5;
while(cnt)
{
fputs(str, fp);
cnt--;
}
fclose(fp);
return 0;
}
2.printf、fprintf、sprintf、snprintf
头文件:stdio.h
函数:int printf(const char *format, ...); int fprintf(FILE *stream, const char *format, ...); int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...);
解析:
printf——默认向显示器打印,很熟悉,不解析。(显示器也是文件,对应标准输出流stdout)
fprintf——指定文件流stream,向指定文件打印;第二个参数format是格式化控制(与printf一样)
sprintf——向缓冲区打印;可以以格式化流的方式,将格式化信息输出到自定义的缓冲区str里。
snprintf——与sprintf一样,多了一个参数size是缓冲区的大小。
注:
Linux下一切皆文件,显示器也是一个文件,所以:若fprintf(stdout, str);
则与printf(str);
一样向显示器打印。
读取——fgets
fgets
头文件:stdio.h
函数:char *fgets(char *s, int size, FILE *stream);
参数:从特定的文件流stream中,按行读取对应的内容,将读到的内容放入缓冲区s中,size是缓冲区的大小
返回值:成功返回字符串的起始地址,失败返回NULL。
2.文件操作系统方案(系统的文件操作接口-系统调用)
打开文件、关闭文件——open、close
open:
头文件:
sys/types.h
sys/stat.h
fcntl.h
函数:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数:
pathname——要打开的文件路径+文件名
flags——打开文件对应的选项(比如读、写)
mode——打开文件的权限(文件的权限,rwx,比如0666)
返回值:
成功返回文件描述符(file descriptor),失败返回-1;
close:
头文件:
unistd.h
函数:
int close(int fd);
open的参数flags的理解
1.系统是如何给一个函数传递多个标志位的?
——比如想给一个函数传递多个标志位,肯定不能设置多个形参比如int func(int flag1, int flag2, int flag3)。又因为传入的标志位flag是int型,有32个比特位,我们可以用1个比特位表示1个标志位,一个整数就可以同时传递32个标志位了——位图。
一般就是使用下面的这种方式传递标志位:
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
#define FOUR 0x8
#define FIVE 0x10
void Print(int flag)
{
if(flag & ONE) printf("ONE\n");
if(flag & TWO) printf("TWO\n");
if(flag & THREE) printf("THREE\n");
if(flag & FOUR) printf("FOUR\n");
if(flag & FIVE) printf("FIVE\n");
}
int main()
{
//测试
Print(ONE);
Print(TWO);
Print(THREE);
Print(FOUR);
Print(FIVE);
printf("----------\n");
Print(ONE|TWO);
printf("----------\n");
Print(ONE|TWO|THREE);
printf("----------\n");
return 0;
}
结果:
而我们在查看open的参数flag的时候,发现有很多参数(如下图所示,没有截全),这些其实就是宏值,每个宏对应的数字比特位是不重叠的,每个对应一个选项,所以学习时学习每个的含义就可以了。
open的参数flag的学习
1.O_CREAT——文件存在就打开,不存在就创建
(一般创建新文件不使用两个参数的open,因为权限会乱码,要使用三个参数的open函数,指定文件权限)
2.O_WRONLY——只写
(O_WRONLY | O_CREAT默认不会对原始文件内容做清空!重复写入的时候会很乱,可以自己尝试)
3.O_TRUNC——对文件内容做清空
4.O_APPEND——追加(没有写入,只是追加)
(追加一般不和清空一起使用,因为逻辑上就有矛盾)
5.O_RDONLY——只读
(读取是对一次已经存在的文件读取,所以使用两个参数的open,不用指定文件权限)
使用例:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LOG "log.txt"
int main()
{
int fd = open(LOG, O_CREAT | O_WRONLY, 0666);
if(fd == -1)
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
else
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
close(fd);
}
此时发现生成的文件权限不是指定的666,而是664,这是为什么?
——因为存在权限掩码umask,umask的计算:最终权限 = 起始权限 & (~umask),普通用户的umask为0002,所以生成的权限是664。
那么我们要如何不让权限掩码影响我们,直接生成指定权限的文件呢?
——调用umask函数,可以设定当前进程启动时,属于自己的umask。
当该进程生成文件的时候,umask用系统的还是该进程的?
——就近原则,离谁更近就用谁的,所以肯定是用当前进程的umask。
umake:设定进程的权限掩码
头文件:
sys/types.h
sys/stat.h
函数:
mode_t umask(mode_t mask);
参数:想要设定的权限掩码
//头文件略
int main()
{
umask(0);
int fd = open(LOG, O_CREAT | O_WRONLY, 0666);
if(fd == -1)
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
else
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
close(fd);
return 0;
}
此时发现生成的文件权限掩码与自己设定的一致。
写入——write
头文件:unistd.h
函数:ssize_t write(int fd, const void *buf, size_t count);
参数:
fd——文件描述符(open的返回值)
buf——缓冲区
count——写入的字符数
将缓冲区中count大小的数据写入到fd中。
返回值:实际写了多少字节,失败返回-1;
读取——read
头文件:unistd.h
函数:ssize_t read(int fd, void *buf, size_t count);
参数:
fd——文件描述符(open的返回值)
buf——缓冲区
count——空间的大小(字节数)
从文件描述符fd中,将数据按数据块的方式读取出来,读取到buf中,读取conut个字节
返回值:读取了多少字节,失败返回-1;
几个系统调用的使用
使用1:
O_WRONLY只写入
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LOG "log.txt"
int main()
{
umask(0);
int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);//打开/创建文件+只写+清空
if(fd == -1)
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
else
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
const char *str = "hello world!";
int cnt = 5;
while(cnt)
{
char line[128];
snprintf(line, sizeof(line), "%s, %d\n", str, cnt);
write(fd, line, strlen(line));//strlen不计算\0,但是这里不+1,因为\0是C语言的规定,不是文件的规定!!
cnt--;
}
close(fd);
return 0;
}
结果1:
使用2:
O_WRONLY | O_APPEND追加写入(不带清空)
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LOG "log.txt"
int main()
{
umask(0);
int fd = open(LOG, O_CREAT | O_WRONLY | O_APPEND, 0666);//追加写入
//int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);//打开/创建文件+只写+清空
if(fd == -1)
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
else
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
const char *str = "hello world!";
int cnt = 5;
while(cnt)
{
char line[128];
snprintf(line, sizeof(line), "%s, %d\n", str, cnt);
write(fd, line, strlen(line));//strlen不计算\0,但是这里不+1,因为\0是C语言的规定,不是文件的规定!!
cnt--;
}
close(fd);
return 0;
}
结果2:
运行多次,可以发现是追加写入。
使用3:O_RDONLY读取
(这里读取的就是使用2中生成的文件)
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LOG "log.txt"
int main()
{
umask(0);
int fd = open(LOG, O_RDONLY);//只读
if(fd == -1)
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
else
{
printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
}
char buffer[1024];
//sizeof会计算\0,IO操作一定要注意\0的问题!记得-1
ssize_t n = read(fd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = '\0';
printf("%s", buffer);
}
close(fd);
return 0;
}
结果3:
读取成功(我们这里是一个一个读取的,不是和C接口一样按行读取,要实现需要精细化处理)
3.库函数与系统调用的关系
C语言的接口(库函数)一定调用了系统调用接口,是上下层关系。
程序员与系统调用接口的关系:
我们平时使用的C、C++库都是对系统调用的封装!
而函数库也是人写的,他们与我们同样不可能绕过OS来对硬件做操作,同时也无法直接操作操作系统,所以只能通过系统调用的方式来操作硬件设备。
同时其他所有语言都是一样的,都要调用这些系统调用接口,只是封装的形式不同!