我们可以用系统调用来操作文件,这种方式和I/O库函数各有千秋,我们需要明白库函数在用户地址空间执行,系统调用是在内核地址空间执行,依赖Linux系统,不要搞混了,那我们下面来学习一下。
文章目录:
- 一、基本概念
- (一)文件描述符
- (二)带缓冲区的I/O && 不带缓冲区的I/O
- (三)man指令
- (四)基础中断机制知识
- 二、系统调用文件I/O函数
- (一)open
- (二)read
- (三)write
- (四)close
- (五)lseek
- 三、系统调用的过程
- 四、例题
一、基本概念
(一)文件描述符
系统调用是在内核空间执行的,那么我们就需要了解在内核中是如何标识文件的。对于内核而言,所有打开的文件都通过文件描述符(简称fd)引用就是标识。文件描述符是一个非负整数,指代被打开的文件,当打开一个现有的文件或创建一个新文件时,内核向进程返回一个文件描述符,将其作为参数给系统调用的I/O操作。
POSIX标准要求每次打开文件时,必须从小到大申请文件描述符,那么最小的文件描述符是几呢?不是0,因为系统已经规定了前三个:
文件描述符 | POSIX名称 (常量定义在头文件<unistd.h>) | 用途 | stdio流 |
---|---|---|---|
0 | STDIN_FILENO | 标准输入 | stdin |
1 | STDOUT_FILENO | 标准输出 | stdout |
2 | STDERR_FILENO | 标准错误 | stderr |
所以最小的文件描述符从3开始,即打开第一个文件open()返回的文件描述就是3。
那么系统创建文件的上限是多少呢?我们可以使用指令来查看最多可以打开多少个文件:
sysctl -a | grep fs.file-max
同时,内核为了防止一个进程创建多个文件,占用文件描述符,对单个进程创建文件的上限也做了规定,可以用:
ulimit -n
默认值为1024个。文件创建的上限是可以更改,在这里我就不阐述了,有兴趣的可以去了解。
(二)带缓冲区的I/O && 不带缓冲区的I/O
带缓冲区I/O就是在内存开辟缓冲区,当执行读文件操作时,先把磁盘文件读到缓冲区,进行一次读取,再根据程序需要将数据给读给变量。我们上次学的库文件I/O函数就是带缓冲区的,如fopen,fread等。这种特性的好处是和外存的交互次数由缓冲区大小决定,如果缓冲区大,和对外存操作少。如果进入内核读取数据,那么交互次数会少,因为它会尽可能地一次读取数据。故执行速度就快,效率高。
不带缓冲区的I/O就是执行I/O函数时,根据程序需求,直接将数据给变量。不带缓冲区I/O依赖于操作系统,即系统级输入输出,就是我们今天要学习的系统调用I/O,如open等,它会和内核进行多次交互,所以执行速度会慢。
但是我们不能以偏概全,不带缓冲区地read系统调用函数一定比带缓冲区地fread库函数速度慢,这时不对的,只有合适,没有不好。我们知道read需要进行多次内核态和用户态的交互,而fread则交互少,如果我们是顺序读取数据,那么fread即带缓冲区的更快,但是如果我们随机读取数据,这时缓冲区的作用降低,read反而速度快。所以要在一定的情景下来找最合适的。
(三)man指令
介绍关于man的指令,方便查看函数的信息:
指令 | 含义 |
---|---|
man 1 command | 查看命令的帮助手册 |
man 2 系统调用 | 查看系统调用手册 |
man 2 库函数 | 查看库函数的使用手册 |
(四)基础中断机制知识
系统调用是由内核提供的一系列函数,实现时涉及到从用户态进入内核态,那么我们如何进入呢?中断,中断是现代计算机不可或缺的机制。
中断的概念: 中断是指CPU在正常运行程序时,由于内部/外部事件或由程序预先安排引起CPU暂停正在运行的程序,转到引起中断的事件程序中去,处理完毕,再返回到继续执行被暂停的程序。
中断的分类:
-
由CPU外部引起的,称为中断,如I/O中断等。
-
由CPU内部事件或程序引起的,称作异常,如数组非法越界等。
-
由在程序中使用了系统调用而引发的过程,称作陷入。
前两个都是被动的,最后一个是主动的,所以把最后一个也称为软中断。
操作系统处理中断: 操作系统中有一张中断表,称为中断描述符表,它保存着中断向量号(即中断序号),对应的异常事件,需要调用的处理程序这三个信息。硬件中断机制提供了256个入口,即包含了256种中断事件类型,它规定:
- 0~31号中断向量被Intel公司保留用来处理异常事件,即操作系统提供对应的异常处理程序,产生一个异常时,找到向量号,调用运行处理程序,处理完成,返回中断点。但是在2.2.5版本的Linux只提供了0~17号中断向量的处理程序,包括了溢出,越界等。
- 0x80(SYSCALL_VECTOR)规定用作系统调用的总入口。所以系统调用函数执行时会触发0x80中断,中断处理函数会通过系统调用函数传来的系统调用号来执行系统调用。
- 其他的都在外部硬件中断源上。
我们还需要了解用户栈和内核栈:
- 用户栈:用户态的程序,如函数的调用在用户栈上执行。
- 内核栈:用来保存中断现场,保存程序间的参数等信息。
那么在程序发生异常或发生系统调用时,陷入内核,产生中断,从用户态到内核态,首先系统会保存现场,将此时所有进程的数据,代码等所有信息保存在内核栈;之后再根据中断的向量号进行中断处理,调用对应的处理程序;最后处理完成后恢复现场,将内核栈信息再还给用户栈,从内核态到用户态。
中断机制是一个很复杂的机制,博主只写了基础知识,如果需要深入了解,自己翻阅资料即可。
二、系统调用文件I/O函数
那我们介绍一下我们常用的系统调用I/O函数吧。
(一)open
open()函数打开或创建一个文件,函数原型为:
# include<fcntl.h>。
int open(const char* pathname,int flags,…)
参数说明:第三个参数的形式我们称为可变参数。
(1)pathname:表示打开或创建文件的路径和名字。
(2)flags:表示打开文件的方式或创建,可以选择的参数为:
参数 | 含义 | 选择 |
---|---|---|
O_RDONLY | 只读打开 | 必须指定,但不能和2,3两个参数任何一个同时存在 |
O_WRONLY | 只写打开 | 必须指定,但不能和1,3两个参数任何一个同时存在 |
O_RDWR | 读写打开 | 必须指定,但不能和1,2两个参数任何一个同时存在 |
O_CREAT | 若文件不存在则创建它,此时第三个参数有效 | 可以选择,不要求必须有 |
O_APPEND | 追加到文件末尾 | 可以选择,不要求必须有 |
还有很多参数,但不常用,就不列举了,多个参数之间使用【|】来连接
(3)可变参数:只有当参数flags种包含O_CREAT创建文件的常量时,这个参数才存在,它要指定文件的权限,格式为【0xxx】,0是一个标识符,读r权限为4,写w权限为2,执行x权限为1。
(4)返回值:成功返回当前未使用的文件描述符,失败返回-1。
(二)read
read()函数从打开文件中读取数据,函数原型为:
# include<unistd.h>
ssize_t read(int files,void*buf,size_t nbytes);
参数含义:
(1)files:文件描述符。
(2)buf:存储读取到的数据的变量。
(3)nbytes:一次读多少字节。
(4)成功返回读到的字节,到达文件结尾返回0,出错返回-1.
读到那里,文件偏移量就到那里,所以以前读过的要想再读,需要移动文件偏移量。
(三)write
write()函数向打开的文件写数据,函数原型为:
# include<unistd.h>
ssize_t write(int files,const *buf,size_t nbytes);
参数含义:
(1)files:文件描述符;
(2)buf:存储需要写入数据的变量。
(3)nbytes:变量存储数据的长度。
(4)返回值:成功返回已写字节数,出错返回-1。
文件偏移量会随着数据写入而移动。
(四)close
close()关闭一个打开的文件,函数原型为:
# include<unistd.h>
int close(int files)
参数说明:files表示文件描述符。成功返回-,失败返回-1。
当一个进程终止时,内核自动关闭它所有打开的文件,这个是close的隐式使用,不是显式。
(五)lseek
lseek()为一个打开的文件设置其偏移值,函数原型为:
# include<unistd.h>
off_t lseek (int files,off_t offset,int whence)
参数说明:
(1)files表示文件描述符。
(2)offset:表示偏移的字节,可正可负和whence参数有关。
(3)whence:表示从哪里开始偏移,可以取值为:
参数 | 含义 |
---|---|
SEEK_SET | 将该文件的偏移量设置为距离文件开始处offset字节 |
SEEK_CUR | 将该文件的偏移量设置为距离当前位置处offset字节 |
SEEK_END | 将该文件的偏移量设置为距离末尾处offset字节 |
(4)返回值:成功返回新的文件偏移量,出错返回-1。
三、系统调用的过程
系统调用是陷入中断,进入内核的过程,是通向操作系统本身的接口,当系统调用I/O函数发生时,内核将调用内核相关函数来实现,如(sys_read(),sys_write()等),系统调用时一个从用户态->内核态->用户态的过程,下面我们来看一下它的过程时怎样的:
- 首先我们要知道系统调用只触发0x80号中断,那么如何确定触发的是那个系统调用呢?在用户空间中有一个表,它保存各个系统调用的地址,包含系统调用函数名,对应内核函数调用号,如read() 1表示read()函数在内核中的sys_read()为1号。
- 我们也知道中断在内核中也有一张中断描述符表,它会根据中断类型来调用对应的处理函数,当我们调用系统调用时,就会产生中断0x80,那么它就会去内核,在内核中根据系统调用表找到需要执行的内核函数,如sys_read(),进行执行即可。
那我们简略的画出这个过程,这个只是为了方便理解画出的图,函数和对应的调用号并不是内核中真正的数据。
四、例题
我们用一下这几个函数,和库函数的例题一样,将界面上输入的数据存储到a.txt中,再将a.txt中的内容显示在终端上,cp拷贝的就不写了,那个只需要替换读写函数即可。这个题目的思路和原来一模一样,不过就是函数变了,取值判断变了,那我们下面来看看代码:
# include<stdio.h>
# include<stdlib.h>
# include<assert.h>
# include<string.h>
# include<unistd.h>
# include<fcntl.h>
int main()
{
int fd=open("./sys.txt",O_RDWR | O_CREAT,0664 );
assert(fd!=-1);
while(1)//循环读取数据
{
printf("input:");
char buff[128]={0};
fgets(buff,128,stdin);//标准输入
if(strncmp(buff,"end",3)==0)
{
break;
}
int n=write(fd,buff,strlen(buff));
if(n<=0)
{
printf("write error");
exit(0);
}
}
printf("------write over----------\n");
int n=lseek(fd,0,SEEK_SET);//重定位
if(n==-1)
{
printf("lseek error");
exit(0);
}
while(1)
{
char buff[128]={0};
int n=read(fd,buff,127);
if(n==0)
{
printf("------read end----------\n");
break;
}
else if(n<0)
{
printf("read error\n");
exit(0);
}
else
{
printf("%s",buff);
}
}
close(fd);
exit(0);
}
结果:
加油哦!。