系统IO
一.引入
在大学期间大家都学过51单片机,我们要利用51单片机去点LED灯,直接去操控单片机内部的寄存器(硬件),这种直接操控单片机的硬件的方式称之为"裸奔",就是说用户与芯片(硬件)之间没有中间层。我们在利用我们的电脑去做一件事的时候(播放音乐),好像并没有接触寄存器之类的硬件,这是因为我们电脑上的芯片的运行方式是"带OS",意思就是我们与硬件之间还有一层,这一层就是OS(Operating System),我们要去实现某个功能,相当于我们把需求告诉OS,那么OS就会帮我们去操作寄存器去实现功能,那么功能实现完毕之后,硬件就会将结果反馈给OS,OS就会将现象呈现给用户。
很明显,"带OS"比"裸奔"更好。这是因为随着芯片的发展,内部的CPU中寄存器的种类和数量越来越多,我们肯定不可能去记住这些寄存器的功能和名字,那么记不住怎么办?我们只能请别人来帮我们,我们称OS是管理我们硬件资源的一种软件,那么肯定包括寄存器。
我们只需要学会怎么去下达命令给OS,非常方便。
那么怎么下达命令给OS?
在linux操作系统下,Linux为了简化我们的工作,提高开发效率,它将对寄存器的操作封装成了接口函数,我们并不需要知道这些函数具体是怎么实现这些功能的,但是我们需要知道这些函数是用来干嘛的。比如,Linux下我们要去打开一个文件,我们只需要调用open函数 即可,我们如果要对文件进行读写操作,只需要调用read/write即可。
我们后续就是要去学习一下这些接口函数的操控。
二.Linux是什么?
Linux是一个开源的操作系统
设计哲学:
Everything is a file,in Linux!
在linux下,一切皆文件,普通文件是文件,设备也是文件
也就是说,在linux下面,操作任何东西,都是操作文件。
在操作系统中有一种软件,叫做文件系统,文件系统的作用是帮助我们管理计算机中纷杂的文件。所以文件系统的完整概念定义为:
操作系统中负责管理和存储文件信息的软件称之为文件系统。
文件系统有很多不同的格式,比如FAT,NTFS,EXT3格式等,格式不同,意味着文件管理,存储文件的方式不同,但是不管是哪一种
文件系统,文件系统会将文件分成两个部分存储:
1. 文件属性
文件的属性比如文件的创建的时间,文件最后一次被修改的时间等时间戳方面的信息,还有文件的大小以及文件的拥有者.....
那么这些文件属性我们用什么东西进行存储?
首先我们肯定不能用数组进行数据的存储,因为各个文件属性的类型不一致,最好就是利用结构体。那么实际上,文件系统就是
用一个结构体去保存这些文件的属性,那么这个结构体名字为 struct inode,我们经常称之为inode节点(有些文献上叫vnode)
2. 文件内容
文件中真正存储的内容。
将图片里面的内容进行一个总结:
硬件:inode节点 --> 文件内容
linux的内核中:
struct inode{}
用来描述一个文件的物理信息,一个文件对应一个inode结点
struct file{}
用来描述一个已经打开的文件
每一个打开的文件都会对应一个struct file。一个文件可以被不同的进程打开。
Linux为了简化文件操作的细节,它会为每一个进程创建一个"进程文件表项":保存每个进程打开的文件的struct file *数组
所以文件的操作过程:
0 --> struct file * --> struct file --> struct inode -->inode结点 -->文件内容
1 --> struct file * --> struct file --> struct inode -->inode结点 -->文件内容
.....
所有我们实际上操作文件其实就是操作0,1....等这样的"文件描述符"
那么文件描述符怎么得到?
通过调用open函数得到的,它的本质上就是"进程文件表项"的下标,是一个int。"文件描述符"用来描述一个已经打开的文件
唯一的ID。后续操作文件(read/write),只需要操作文件描述符
那么像上面用的这种操作文件的函数:open,read,write等接口函数,我们称之为"系统IO"
三. 系统IO
在linux系统文件操作的接口函数
操作文件的步骤:
(1)打开文件
(2)操作文件
(3)关闭文件
1) 打开文件 open
NAME
open - open and possibly create a file
open在linux下,用来打开或者创建(创建后打开)一个文件
头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数原型:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数参数:
@pathname: 要打开或者创建的文件名,记得带路径。
如果不带路径,就是当前路径
"./1.txt"
@flags:打开文件时的标志。
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 可读可写
以上的三个选项必须选择一个并且只能选择一个
O_APPEND :追加,打开文件后,文件光标在文件末尾
O_CREAT :创建,如果文件不存在的话,则就会先创建再打开
O_EXCL :这个标志跟上述的O_CREAT配合使用,用来测试文件是否存在的,如果参数二指定O_CREAT|O_EXCL,如果文件
存在,就会失败
O_TRUNC :截短标志,假如文件存在,并且是一个普通文件,而且打开的方式是O_WRONLY或者O_RDWR,则会先清空文件的
内容。如果是O_RDONLY|O_TRUNC,不会起作用
O_NONBLOCK :非阻塞方式打开文件
非阻塞:"不等待"
如果文件没有内容,read不会阻塞,直接返回一个错误,如果文件没有空间,write不会阻塞,直接返回一个错误
阻塞: "等待"
如果文件没有内容,read会阻塞(直到有数据或出错)
如果文件没有空间, write会阻塞(直到可写或出错)
如果上述想用多个选项,则选项之间要用 | 进行连接
@mode : 权限
这个参数只有当创建文件的时候,也就是上面第二个参数有O_CREAT的时候才需要第三个参数。如果不是创建文件
第三个参数不需要,第三个参数的意思是指定新创建文件的权限,有两种方式可以指定:
a. 宏
S_IRWXU 用户可读可写可执行
S_IRUSR 用户可读
S_IWUSR 用户可写
S_IXUSR 用户可执行
S_IRWXG 组用户可读可写可执行
S_IRGRP 组用户可读
S_IWGRP 组用户可写
S_IXGRP 组用户可执行
S_IRWXO 其它用户可读可写可执行
S_IROTH 其它用户可读
S_IWOTH 其它用户可写
S_IXOTH 其它用户可执行
比如:S_IRUSR|S_IRGRP|S_IROTH ==> r--r--r--
b. 用八进制来表示
0666 ==> 110110110 ==> rw-rw-rw-
返回值:
成功返回一个int整数-->文件描述符(>2)
因为操作系统会为你每个进程都会自动打开三个文件:
标准输入文件 文件描述符为STDIN_FILENO (0)
标准输出文件 文件描述符为STDOUT_FILENO (1)
标准出错文件 文件描述符为STDERR_FILENO (2)
失败返回-1,同时errno将被设置
例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc,char *argv[])
{
int fd = open(argv[1],O_RDWR);
if(-1 == fd)
{
printf("open %s failed!!\n",argv[1]);
printf("errno == %d\n",errno);
perror("open failed");
return -1;
}
printf("OK!!\n");
printf("fd == %d\n",fd);
return 0;
}
以上例子中一旦open出错,将会返回-1,但是我们能知道是因为什么导致出错的吗?如果我们想知道出错的原因,我们就需要利用
errno。errno就是一个全局变量,我们可以man一下errno,它的作用其实就是用来保存最后一个出错的错误码。
errno定义在#include <errno.h>
但是知道错误码还是不知道是什么错误。
perror把错误码对应的提示打印出来
perror("open failed");//它是无缓冲,所以不需要换行符
==> open failed : 提示信息
(2)操作文件(read/write/lseek.....)
read
NAME
read - read from a file descriptor
从文件描述符内读取内容
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
@fd:文件描述符,表示你想从哪一个文件里面读取 -->open函数的返回值
@buf:指向一块内存的首地址,表示你要把文件里面读取到的内容保存到哪里去 -->数组
@count: 表示你想读取多少个字节的数据
返回值:
返回大于0或者等于0都表示读取成功:
>0 :返回你实际上读取到的字节数(<=count)
=0 :表示已经读取到了文件末尾或者文件为空
失败返回-1,同时errno将被设置
----------------------------------------------------------
write
NAME
write - write to a file descriptor
向一个文件描述符内写入数据
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
@fd:文件描述符
@buf:指向一块内存,这个内存里面存放的是你即将要写入的数据
@count: 想写入多少个字节的数据
返回值:
>0 表示实际上写入的字节数
==0 表示什么也没写
==-1 表示出错,同时errno被设置
例子:
int main(int argc,char *argv[])
{
printf("argc==%d\n",argc);
printf("argv[0]:%s\n",argv[0]);
printf("argv[1]:%s\n",argv[1]);
printf("argv[2]:%s\n",argv[2]);
int fd = open(argv[1],O_RDWR);
if(-1 == fd)
{
perror("open failed");
return -1;
}
char buf[100]="helloworld!!";
int ret = write(fd,buf,strlen(buf));
if(-1 == ret)
{
perror("write failed");
return -1;
}
printf("ret == %d\n",ret);
return 0;
}
假如有一个文本4.txt : aaabbbccc
打开这个文件后,首先读取了3个字节的内容并打印
然后再写入3个字节的内容: 111
最后请猜测4.txt里面的内容:
aaa111ccc
从上述的测试就能够得到文件有个"光标"属性,当我们打开文件的时候,此时光标在文件的开头,此时称之为文件的偏移量(光标距离文件开头的字节)为0,此时我们每次进行read/write成功的操作都会使光标往后移动,移动的字节跟你read/write的字节一样
--------------------------------------------------------
lseek
NAME
lseek - reposition read/write file offset
移动读/写的光标偏移量
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
@fd:文件描述符
@offset:偏移量,就是你本次想要改变的大小
>0 :往后偏移
==0 :不偏移
<0 :向前偏移
@whence:定位方式,有如下的三种:
SEEK_SET : 基于文件的开头位置
SEEK_END : 基于文件的末尾位置
SEEK_CUR : 基于文件的当前位置
例子:
lseek(fd,-5,SEEK_END);//基于文件末尾再往前偏移5个字节
lseek(fd,0,SEEK_END);//新光标就在文件末尾
返回值:
成功的话,返回新光标距离文件开头的字节数
失败返回-1
例子:
int size = lseek(fd,0,SEEK_END);//size表示就是文件的大小
---------------------------------------------------------------------------------
3)关闭文件
close
NAME
close - close a file descriptor
关闭一个文件描述符
SYNOPSIS
#include <unistd.h>
int close(int fd);
@fd:就是你想要关闭的那个文件的文件描述符