系统调用
文件描述符
每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的 信息,称为进程描述符。task_struct中有一个指针(struct files_struct *files; )指向files_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开的文件的指针,如下图所示
用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引 (即0、1、2、3这些数字),它实际上是一个由内核保存的数组下标,所以不会是负数。这些索引就称为文件描述符(File Descriptor),用int 型变量保存。 当调用open 打开一个文件或创建一个新文件时,内核分配一个文件描述符并返回给用户程序,该文件描述符表项中的指针指向新打开的文件。当读写文件时,用户程序把文件描述符传给read 或write ,内核根据文件描述符找到相应的表项,再通过表项中的指针找到相应的文件。
文件描述符数组在每个进程中都会持有一份,所以理论上是每个进程最多可以打开 1024 个文件,而不是系统中所有的进程一共只能打开 1024 个文件。
例:
|
|
具体的系统调用I/O
fileno(3)
|
|
这个函数的作用是从 STDIO 的 FILE 结构体指针中获得 SYSIO 的文件描述符。
fdopen(3)
|
|
这个函数和上面的 flieno(3) 函数的功能是反过来的,作用是把 SYSIO 的文件描述符转换为 STDIO 的 FILE 结构体指针。mode 参数的作用与 fopen(3) 中的 mode 参数相同,这里不再赘述。
虽然这两个函数可以在 STDIO 与 SYSIO 之间互相转换,但是并不推荐对同一个文件同时采用两种方式操作。因为 STDIO 和 SYSIO 之间它们处理文件的私有数据是不同步的,如果同时使用两种方式操作同一个文件则可能带来不可预知的后果,具体可以参考上一篇博文中提到的那个合并系统调用的例子。
open(2)
|
|
想要使用 SYSIO 操作文件或设备,要先通过 open(2) 函数获得一个文件描述符。注意博文中在函数上方标识出来的头文件大家在使用这个函数的时候一定要一个不少的全部包含到源代码中。
参数列表:
pathname:要打开的文件路径。
flags:指定文件的操作方式,多个选项之间用按位或( | )运算符链接。
比选项,三选一:O_RDONLY, O_WRONLY, O_RDWR
可选项:可选项有很多,这里只介绍常用的,想要查看完全的可选项,可以查阅 man 手册。
选项 | 说明 |
---|---|
O_APPEND | 追加到文件尾部。 |
O_CREAT | 创建新文件。 |
O_DIRECT | 最小化缓冲。buffer 时写的加速机制,cache 是读的加速机制。 |
O_DIRECTORY | 强调一定要打开一个目录,如果 pathname 不是目录则会打开失败。 |
O_LARGEFILE | 打开大文件的时候要加这个,会将 off_t 定义为 64 bit,当然也可以在编译的时候使用上一篇博文提到的宏定义来指定 off_t 的长度。 |
O_NOFOLLOW | 如果 pathname 是符号链接则不展开,也就是说打开的是符号链接文件本身,而不是符号链接指向的文件。 |
O_NONBLOCK | 非阻塞形式。阻塞是读取不到数据时死等,非阻塞是尝试读取,无论能否读取到数据都返回。 |
mode:8 进制文件权限。当 flags 包含 O_CREAT 选项时必须传这个参数,否则可以不用传这个参数。当然系统在创建文件的时候不会直接这个参数,而是通过如下的公式计算得到最终的文件权限:
mode & ~(umask)
具体的 umask 的值可以通过 umask(1) 命令获得。通过这样的公式进行计算可以避免程序中创建出权限过高的文件。
不知道小伙伴们注意到没有,这个函数有一个有趣的地方。C 语言中是没有函数重载这个概念的,那么为什么这两个 open(2) 函数很像重载的函数呢?实际上它们是用可变长参数列表来实现的。
顿时让我想起来一道面试题:如何确定一个函数是用重载实现的还是用变长参数实现的?答案是给它多传几个参数嘛,如果报错了那一定是函数重载,否则就是变长参数实现的呗。
close(2)
|
|
关闭文件描述符。
参数是要关闭的文件描述符。注意当一个文件描述符被关闭之后就不能再使用了,虽然 fd 这个变量的值没有变,但是内核已经将相关的资源释放了,这个 fd 相当于一个野指针了。
返回值:
成功为0,失败为-1。但很少对它的返回值做校验,一般都认为不会失败。
read(2)
|
|
这是 SYSIO 读取文件的函数,作用是从文件描述符 fd 中读取 count 个字节的数据到 buf 所指向的空间。
返回值:返回成功读取到的字节数;0 表示读取到了文件末尾;-1 表示出现错误并设置 errno。
注意 read(2) 函数与 STDIO 中的 fread(3) 函数的返回值是有区别的,fread(3) 返回的是成功读取到了多少个对象,而 read(2) 函数返回的是成功读取到的字节数量。
write(2)
|
|
write(2) 是 SYSIO 向文件中写入数据的函数,作用是将 buf 中 count 字节的数据写入到文件描述符 fd 所对应的文件中。
返回值:返回成功写入的字节数;0 并不表示写入失败,仅仅表示什么都没有写入;-1 才表示出现错误并设置 errno。
注意 write(2) 函数与 STDIO 中的 fwrite(3) 函数的返回值是有区别的,fwrite(3) 返回的是成功写入了多少个对象,而 write(2) 函数返回的是成功写入的字节数量。
大家想一想,为什么会出现写入的值是 0 的情况呢?其实原因有很多,其中一个原因是当写入的时候发生了阻塞,而阻塞中的 write(2) 系统调用恰巧被一个信号打断了,那么 write(2) 可能没有写入任何数据就返回了,所以返回值会是0。至于什么是阻塞,什么是信号,LZ 会在后面的博文中讲解。
lseek(2)
|
|
通过上一篇博文大家知道了文件位置指针这个概念,它是系统为了方便我们读写文件而设定的一个标记,随着我们通过函数对文件的读写,它会自动相应的向文件尾部偏移。
那么是不是说当我们读取过文件的一段内容之后,就没办法回去再次读取同一段内容了呢?
其实不是的,通过 lseek(2) 函数就可以让我们随心所欲的控制文件位置指针了。
参数列表:
fd:要操作的文件描述符;
offset:想对于 whence 的偏移量;
whence:相对位置;三选一:SEEK_SET、SEEK_CUR、SEEK_END
SEEK_SET 表示文件的起始位置;
SEEK_CUR 表示文件位置指针当前所在位置;
SEEK_END 表示文件末尾;
返回值:
成功时返回文件首相对于移动结束之后的文件位置指针所在位置的偏移量;失败时返回 -1 并设置 errno;
这个函数的 offset 参数和返回值都对基本数据类型进行了封装,这一点要比标准库的 fseek(3) 更先进。
写一段伪代码来说明这个函数的使用方法。
|
|
lseek使用
系统每次打开open一个文件都会保存一个指向文件当前位置的指针,当读写操作完成时,指针会移到下一个记录位置,这个指针与文件描述符(int fd)
相关联。
所以write只会更新下一条记录,而不是修改当前找到的那条要修改的记录。
那么问题就来了。
问题:在文件操作中,如何改变一个文件的当前读/写位置?
答题:使用系统调用lseek
目标:是指针指向文件中指定的位置
- 1off_t oldpos = lseek (int fd, off_t dist , int base)
fd 文件描述符 , dist 移动的距离 , base (SEEK_SET:文件开始位置,SEEK_CUR:当前位置 ,SEEK_END:文件结尾)
返回值:oldpos 指针变化前的位置 。遇到错误返回 -1
time(1)
之前讨论过 STDIO 与 SYSIO 的效率问题,所以在这里聊一聊 time(1) 命令。
这个命令可不是用来查看系统当前时间的,想要查看系统时间得使用 date(1) 命令,这个不是我们今天要讨论的内容,所以就不说了。
time(1) 命令的作用是监视一个程序的用户时间,从而可以粗略的帮助我们分析这个程序的执行效率。
|
|
这是一个模仿 cp(1) 命令的程序的核心部分代码,其中的 buf 是一个 char 数组,用来作为数据读写的缓存。当 buf 的容量不同时文件拷贝的效率也是不同的,因为过于频繁的执行系统调用和使用过大的缓存都会使效率下降。如果通过不停的修改 buf 的容量的方式测试 buf 为多大的时候拷贝效率最高的话,就可以使用 time(1) 命令监视程序的执行时间。
>$ gcc -Wall mycp_sysio.c -o mycp_sysio
>$ time ./mycp_sysio rhel-server-6.4-x86_64-dvd.iso tmp.iso
real 1m30.014s
user 0m0.003s
sys 1m29.003s
sys 是程序在内核态消耗的时间,也就是执行系统调用所消耗的时间。
user 是程序在用户态消耗的时间,也就是程序本身的代码所消耗的时间。
real 是用户等待的总时间,是 sys + user + CPU 调度时间,所以 real 时间会稍微比 sys + user 时间长一点。一个程序从提高响应素的的方式提高用户体验,一般指的就是提高 real 时间。
其它小结
linux下可以通过以下系统调用来操作文件:
123456open(filename,how)creat(filename,mode)read(fd,buffer,amt)write(fd,buffer,amt)lseek(fd,distance,base)close(fd)进程对文件的读,写都要通过文件描述符,文件描述符表示文件和进程之间的连接。
每次系统调用都会导致用户模式和内核模式的切换以及执行内核代码,所以减少程序中的系统调用发生的次数可以提高程序的运行效率。
程序可以通过缓存技术来减少系统调用的次数,仅当写缓冲区满或读缓冲区空才调用内核服务。
内核就是通过内核缓冲来减少访问磁盘的IO次数。
参考资料: