APUE_Chapter03_文件IO_笔记总结

            ***PAY A TRIBUTE TO W.Richard Stevens***

Chapter03: FILE I/O

3.1 简介

read, write, lseek, close, open这五个函数就能覆盖几乎大部分的Unix上的IO. 我们这章主要探讨的就是无缓存IO(unbuffered I/o), 相比于unbuffered IO就是第五章会讨论到的缓存IO. unbuffered IO并不是ISO C的标准, 但是是POSIX.1和SUS的部分. 无缓存就是意味着每次read/write都是直接系统调用. 我们还会涉及到进程间操作的原子操作. 我们也会讨论进程间文件是怎么共享的,用到了什么数据结构. 之后会讨论dup, fcntl, sync, fsync和ioctl. 这些都是重点,开启IO之旅吧~~

3.2 文件描述符

对于内核来说,所有打开的文件都会指向一个文件描述符. 当我们打开一个已经创建或者创建一个新的文件时,内核都会返回到相应的进程中一个文件描述符,当我们进行文件的read/write时,其中传入的参数之一就是open/create生成的文件描述符.
在Unix中一般情况下来说,0表示标准输入,1表示标准输出,2表示标准错误. 这些并不是内核的特点而是很多应用程序使用的,但是很多应用还是经常打破这个规定的.
既然遵循POSIX.1规则,那么这些magic就应该也被替换掉根据POSIX, 所以在unistd.h中定义了0是STDIN_FILENO, 1是STDOUT_FILENO, 2是STRERR_FILENO.
这里写图片描述
那么文件描述符能创建多少呢?这个值是0~OPEN_MAX-1. 在早期的Unix中最大只能打开20个位每个进程,但是之后增加到了63个. 但是FreeBSD 8.0, Linux 3.2.0, MAC OS X10.6.8和Solaris10将这个设置成了能打开无数个了,具体数量是和内存,整型大小,以及一些系统管理员设置有关了.

3.3 open(2) openat(2)

这里写图片描述
这俩函数后面的…这是ISO C的标准,表示参数的数量和类型是可变动的. 具体有什么用处我们稍后讨论. open函数的path表示的是打开或者要创建的文件的名字. oflag在fcntl.h中定义定宏这些值用”|”进行衔接,当然我们在manual page中也能看到:
这里写图片描述
O_RDONLY: 以只读模式打开文件
O_WRONLY: 以只写的模式打开文件
O_RDWR: 以可读写的模式打开文件,大部分的实现为了兼容旧实现都有O_RDONLY用0表示,O_WRONLY用1, O_RDWR用2.
O_EXEC: 以只能执行的模式打开文件
O_SEARCH: 以只能进行搜索的模式打开文件,这个一般用于文件是目录的情况.这个主要是为了检测检索目录是否有权限当打开一个目录时,
大部分的系统都没有支持这个模式.
O_APPEND: 每次写的时候都能够添加到文件的末尾
O_CLOEXEC: 设置FD_CLOEXEC文件描述符标志.
O_CREAT: 如果文件不存在就创建. 这个模式就需要有第三个参数了, 或者openat中的第四个参数–mode, 指定了文件的访问权限. 我们会之
后讨论如何通过umask来设置这个值.
O_DIRECTORY: 如果路径不是指定到一个目录的话,那么就会返回错误.
O_EXCL: 如果文件已经存在了还指定了O_CREAT,那就报错. 这个主要是为了检测是不是已经创建了文件了,如果没有的话会原子操作创建
文件.
O_NOCTTY: 当路径指定到了终端设备上,那么久不要为这个进程分配控制终端.
O_NOFOLLOW: 如果path指定到了一个符号链接,那么就返回错误.
O_NONBLOCK: 如果path指定到了FIFO, 块文件, 或者一个字符文件,这个选项则将他们进行IO操作时为非阻塞状态. 在release of System
V中用到的是O_NDELAY(no delay). 这个和O_NONBLOCK是很像的,但是O_NDELAY的返回值很操蛋,如果在pipe, fifo, 或者字符文件中
没有读到的数据,那么就返回0, 但是当数据读完后也是返回0. 所以在老版本的SVR中依然用的这个,新的也用了O_NONBLOCK.
O_TRUNC: 如果文件是存在的,并且有只写或者读写功能的时候,就将长度truncate成0.
O_TTY_INIT: 当打开一个没有打开的终端设备的时候,设置termios结构体表示遵循SUS标准.
O_DSYNC: 每次write的时候也会等待物理IO的完成,但是如果不影响写入的数据的读的话,就不会更等待更新文件的属性的. _DSYNC和
O_SYNC很像但是有着细微的不同, O_DSYNC更新文件的属性只会在影响到文件数据的时候才做. 也就是说O_SYNC当写入时,会同步
更新时间等属性,但是O_DSYNC就不会了.
O_RSYNC: 在每次读操作时会等待在这个文件相同部分的写操作的完成. Linux中会将这个标志位同O_SYNC一样的功能. Mac OS不支持
O_RSYNC, 但是定义了O_DSYNC与O_SYNC一样的功能. Solaris 支持上面三个. FreeBSD有O_FSYNC和O_SYNC一样的功能, 但是不
支持O_DSYNC和O_RSYNC.
当我们使用open或openat函数时返回的文件描述符就是最低的文件描述符. 这样的话,我们经常会有操作是将这个文件的结果作为标准输入或标准输出,那么我们就先关闭0或者1号描述符,然后open之后返回的文件描述符就是0或1. 这个案例经常会用在pipe操作中.
那么我们说说openat函数和open的不同,openat多了个fd是什么鬼,这里有三点情况会用到fd:
1. 如果path参数是一个绝对路径,那么这个fd就废了也就是说你设置啥玩意儿都没用了,函数就相当于是open了.
2. 如果path是一个相对路径,这时fd就是它所属的目录的文件描述符.
3. 如果path是一个相对路径,并且fd是AT_FDCWD, 那么表示以程序的当前路径表示根目录,也就是说path就是绝对路径了.
openat函数是POSIX.1的后期加入的,加入是为了解决两个问题:
1. 给线程一种能够以相对路径的方式单开文件的方式, 因为一个进程中的线程都是共享进程资源的,并且它们共享当前目录,也就是进程目录,这样就很难让同一进程中的多线程工作在不同的目录下.
2. 防止TOCTTOU(time_of_check_to_time_of_use)问题, 简单点说就是,TOCTTOU错误就是当我们有两个基于文件的函数调用的时候,第二个函数需要第一个函数的结果,它们并不是原子操作,所以这样,在他们间隙就可能发生结果篡改,导致真个程序崩溃. 这一般会用在找寻一个权限级别高的文件的安全漏洞处.
咱们再讨论一下文件名超限制的问题,这是个历史性的问题,对于不同的文件系统有着不同的标准. 一般这个宏是NAME_MAX, 一般是14个character, 但是如果超了(比如说15个character)怎么处理?在SVR2中,会静悄悄的将文件名截断,也就是只保留前14个character, 但是这就会产生很多问题, 因为当我们用open, stat这些函数的时候,传入的文件名就无法判断初始是不是原始的文件名呢?但是在BSD驱动的系统上,就会直接返回错误,并且errno=ENAMETOOLONG. 于是我们的老大哥POSIX.1出马了, 它用_POSIX_NO_TRUNC来决定你是截断呢还是返回错误码. 这个值对于不同的文件系统是会变化的,我们可以通过之前提到的fpathconfpathconf来查询支持哪种. 这本身就是讨论很久的问题,基于SVR4的文件系统S5就不会返回错误码,但是基于BSD的文件系统UFS就会返回错误码. BSD驱动的系统和Linux都会返回错误码.
当_POSIX_NO_TRUNC生效的时候,errno=ENAMETOOLONG, 并且返回错误,当超过NAME_MAX. 但是现代的操作系统的限制都达到了255个character, 因为一般情况下文件名都会小于这个值的,所以也是避免历史性的纠结问题的发生.

3.4 creat(不是create)

这里写图片描述
这个函数主要出现是因为以前的open函数在文件没有存在的情况下不能打开,所以就出下了creat函数,但是现在都creat就等价于:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
也就是现在很少用creat这个函数了.

3.5 close

这里写图片描述
当我们调用close的时候,就释放了进程在这个文件上的记录锁. 但是进程结束的时候,内核会帮我们将该进程中打开的fd全部关闭掉,所以基于这个特点,close也是用的并不是太多了.

3.6 lseek

这里写图片描述
每当我们打开一个文件的时候,都会关联一个当前文件的偏移量的值, read和write函数通常都是从当前文件的偏移量开始的,然后返回偏移量从当前到读或者写了的字节数. 通常情况下当我们打开一个文件的时候(write, open, read),当前文件偏移量都会设置成0. 除非open的时候O_APPEND设置进去.但是如果这个文件一直打开着,当我们写一次,offset就会存储为SEEK_END的值,也就是说我们的读取文件从哪里读就是根据这个offset来决定的,当我们调用lseek这个函数的时候内核就会将offset指定成这个函数的返回值.
然后我们说说单独操作偏移量的函数lseek这个l表示是long integer的意思, 这个函数中的offset是取决于whence的:
如果whence=SEEK_SET, 那么文件的offset就是从文件开始加上offset.
如果whence=SEEK_CUR,那么文件的offset就是文件的当前offset加上offset,这里的第二个参数可以正负.
如果whence=SEEK_END, 那么文件的offset就是从文件的结尾offset加上offset,这里的第二个参数可以正负.
由于lseek函数成功返回后是返回当前文件的偏移量,所以,我们可以用来用offset=0和SEEK_CUR来测试文件当前的位置在哪里. 我们也能验证这个fd能不能被seek. 比如说如果是pipe, FIFO, socket是不能被seek的. 它们会返回-1, 并且errno=ESPIPE.

#include <stdio.h>
#include <unistd.h>
int main(void)
{
    if(lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
    {
        printf("cannot seek\n");
    }
    else{
        printf("seek ok\n");
    }
    return 0;
}

这里写图片描述
那有人会有疑问,如果whence设置为SEEK_END, 然后offset是正整数,是什么情况? 这就引出了文件空洞的概念,这在Unix系统上是允许的, 这样的话,我们没有写入的字节都是0,当我们下次再写入的时候,这个文件就进行了大小上的扩展,但是这些数据都会在磁盘上吗?其实第一次偏移超出时是没有占用磁盘空间的,当我们写入时才会占用磁盘,但是如果第一次写入的文件结尾到第二次写入的开头有空隙的话,这部分是不会在磁盘分配空间的,这就是文件空洞现象. 这是合理的,还有一点这个查询偏移量的记录是直接又内核保存了的,并没有进行任何的IO操作.
我们来测试一些文件空洞现象:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
#define FILE_MODE       (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
int main(void)
{
    int fd;
    if((fd = creat("file.hole", FILE_MODE)) < 0)
    {
        perror("creat error");
    }
    if(write(fd, buf1, 10) != 10)
    {
        perror("perror err");
    }
    /*offset now is 10*/
    if(lseek(fd, 16384, SEEK_SET) == -1)
    {
        perror("lseek error");
    }
    /*offset now is 16384*/
    if(write(fd, buf2, 10) != 10)
    {
        perror("buf2 write err");
    }
    /*offset now is 16394*/
    return 0;
}

这里写图片描述
这里写图片描述
针对上面说到的不调用lseek而是直接用两次write会是覆盖呢还是往后写呢?如果理解了一开始那句话就能理解什么个情况:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
#define FILE_MODE       (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
int main(void)
{
    int fd;
    if((fd = creat("file.hole", FILE_MODE)) < 0)
    {
        perror("creat error");
    }
    if(write(fd, buf1, 10) != 10)
    {
        perror("perror err");
    }
    if(write(fd, buf2, 10) != 10)
    {
        perror("buf2 write err");
    }
    return 0;
}

这里写图片描述
想一想也是情理之中的,既然你文件描述符关了,内核咋还能给你存储这个offset呢, 开玩笑,那存下多少了….

3.7 read

这里写图片描述
如果成功返回,就返回读到的字节数,如果读到结尾就返回0. 但是有几种情况是读取到的值是异常的(这个是重点, 项目中肯定会碰到这样的问题):
1. 如果我们期望读到的文件结尾的时候还没有达到nbytes的数量,那么就直接返回读到的字节数,这样的话就会小于nbytes.
2. 当从终端设备进行读取的时候,一般情况下都是一次读取一行.
3. 当从网络中读取的时候, 缓存的情况可能造成读取到的小于nbytes.
4. 当从pipe或者FIFO中进行读取的时候, 如果pipe包含的字节数少于nbytes, 那么就返回能获得的字节数.
5. 当从记录设备比如说从磁盘中读取的时候,返回的数量就是一次记录的数量.
6. 当被终端打断或者有部分数据已经被读取了的时候返回的值小于nbyte.
同样的,read的读取也是根据offset来作为起始.
知道吗, 上图中的这个函数是由POSIX.1几经改变后的,那么原型是什么样呢?
int read(int fd, char *buf, unsigned nbytes);
我们能够看到从char 变为了void , 这主要是为了与ISO C保持一致,因为在ISO C中void *是定义通用指针的方式.
另一个返回值变成了ssize_t, 那么ssize_t是什么呢?我们通过找头文件,我们能发现ssize_t: 在32位计算机系统中,ssize_t 是int型,占4个字节,在64位计算机系统中,ssize_t是long int 型,占8个字节. 那么size_t就是相对应的signed整形了. 当然定义成ssize_t而不直接写成signed int 就是因为我们之提到的,为了适应不同的平台而定义的类型.
然后POSIX.1更新的方法要求返回值是正整数,ssize_t, 0是文件结束,-1是错误.

3.8 write

这里写图片描述
一般write函数的返回值和nbytes是一样的,如果不一样就返回错误,发生错误一般就是磁盘满了,或者超过了这个进程的限制. write函数也是从当前offset开始写起,然后offset一点一点的移动根据写了的字节数.

3.9 I/O效率

我们先码个用read和write从终端读写的代码:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#define BUFFSIZE 4096
int main(void)
{
    int n;
    char buf[BUFFSIZE];
    while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
    {
        if(write(STDOUT_FILENO, buf, n) != n)
        {
            perror("write err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    return 0;
}

这里写图片描述
这里我们就会有疑问,到底这个BUFFSIZE取多少合适的?
我们通过进行不同BUFFSIZE的测试可以得出这样的结果:
这里写图片描述
我测试的系统是ext4文件系统,ext4的文件系统的一个block是4096bytes, 所以说当BUFFSIZE设置成4096和更大时系统CPU时间能达到最小,但是即使大于4096的buffer也是对系统CPU时间影响不大了.
但部分的文件系统都会有个提前读的性能提升的方式,也就是说内核会先读取大于指定的nbytes的大小,也就是说如果发现是按顺序的在读这个buffer的时候,就多往后读写并且假设应用程序会很快读到. 所以能看出来时钟周期从32到更大的时候都是基本一致的.

3.10 File Sharing

这里写图片描述
Unix是允许在不同的进程中共享打开的文件的,这就是我们即将引入的dup, 但是在引入这个函数之前,我们先介绍一下内核对IO支持的数据结构.
1. 每个进程在进程表中(entry)都有一个记录项,在每个记录向中都有一个打开的文件描述符表, 可以把它看成是一个向量, 每一个文件描述符占用一项, 与每个文件描述符相关联的是:
·文件描述符标志(close_on_exec, 这个我们稍后讲有些什么)
·指向文件记录向表的指针.
2. 内核为每个打开的文件都维护一个文件记录项表,在这个表中包含:
· 文件的状态标志(读,写, append, sync和非阻塞)
· 当前文件的偏移量
· 指向v-node的指针
3. 每个v-node中都存放着文件类型的信息和在这个文件上操作的函数的指针. 对于大部分的v-node都会有一个指针又指向i-node. i-node的信息就是用来当文件打开的时候从磁盘上读取信息的,也就是获取一些永久性的信息. 比如,i-node包含文件的所有者,文件的大小,指向实际中在磁盘上block位置的指针等等. 但是在linux上没有v-node这个东西,但是有一个公用i-node, 其实这俩是一样的,都会指向i-node.
打开的文件描述表可能会以链表的形式存储而不会数组,但是不管怎么样总体概念是一样的.
那为什么要引入v-node呢?主要是为了支持同一个电脑系统上的不同文件系统. 这也就是由Bell实验室和Sun的两位大师独立完成并命名为VFS, 独立的文件系统的i-node就叫做v-node. 在之后所有使用了Sun的NFS(Network file system)的代码的产商都将这个v-node也加入了, BSD系统第一次加入v-node也是第一次加入NFS时候的4.3BSD.
在SVR4中也取缔了SVR3中文件系统独立的i-node用v-node. Solaris就是基于SVR4的。但是在linux中是用了独立的文件系统的i-node和非独立文件系统的i-node。
那么当两个独立的进程打开了同一个文件时:
这里写图片描述
从图中可以看出,它们其实是从v-node才开始一样的,各自还维护者各自的文件记录向表,这就是为了为不同的进程维护它们当前的偏移量.
这里我们用我们之前的writelseek分析一波:
1. 当写操作结束后, 当前文件的在file table entry里的offset将增加写入的字节数,如果当前offset超过了i-node里的文件大小,那么i-node中的文件大小就设置成文件当前的偏移量的值.
2. 如果一个文件用O_APPEND标志打开的时候,相应的file table entry里面的file status flags设置为O_APPEND, 然后去获取i-node中文件的大小并将offset设置成这个值.
3. 如果文件用lseek指向了文件的末尾,那么就是从i-node取出size然后将offset设置成这个值(这个和O_APPEND是不一样的,涉及到原子操作的问题,我们马上讨论)
4. 这样我们就很明显的看出来,lseek是没有进行IO的.
那有没有可能多个打开的fd记录向指向同一个file table entry呢?当然是可能的,dup和子父进程就是这样的,我们稍后讨论.
我们还需要注意的一点是fd flags和file status flags是不同的,前者是针对的单个的进程中单个的打开的文件描述符,但是后者是可以作用于多个进程中所有的文件描述符的. 我们之后会引入fcntl函数,能够实现更改这两个变量的功能.
当多个进程同时写入同一个文件的时候是怎么避免冲突的呢?

3.11 原子操作

在古时候,是没有O_APPEND这么牛逼的方法的,那怎么往最后面加呢?比如说A和B两个进程,分别有着自己的file table entry,但是它们打开了同个文件,也就是v-node是一样的,假设A调用了lseek,然后将A的offset设置成1500(也就是当前文件的末尾), B这时调用了write,将B的offset为1600, 因为当前的offset大于了i-node中的size,所以内核就将i-node中的size变为1600,然后内核又切换到A, A开始写的时候是从offset=1500开始写的,这样就覆盖了B之前写的.
问题就是出在了先定位然后再写,这样用两个函数去完成,就很恐怖. 那么只要将这两个函数绑定成一个原子操作就可以了,那么就是打开文件的时候设置O_APPEND, 这样就能使得内核在每次写操作之前,都将进程offset移动到文件末尾(也就是首先从i-node的size获取)然后直接写. 这样就不存在说用lseek设置好后然后进程被抢走CPU资源,因为单纯的write这种函数是不会去i-node中去获取当前的额长度的,而是无脑从offset中取,当资源回来的时候是执行write的时候,就直接从之前存下的offset中获取值,这就是bug的问题所在.
规定制定者们考虑到这点后由SUS进行开发了一对读写方法:
这里写图片描述
执行pread就相当于是是先执行了leek然后执行了read. 但是也是有些区别的:
1. 当我们使用pread的时候是没有办法中断两个过程的发生的.
2. 用pread或者pwrite并不会更新当前offset的.
当然在我们之前提到的open函数中也是定义了O_CREATO_EXCL也是为了当我们创建文件的时候进行原子操作,因为当我们创建文件的时候也是分步进行的,如果创建了那么就报错,如果没有创建就创建,是有个先判断的过程的. 具体分析过程和read,write类似,就不细说了.

duo和dup2

这里写图片描述
dup函数会返回最小的没有打开过fd, dup2中的fd2表示新打开的fd, 如果这个指定的fd已经打开了,那么就先关闭,再返回这个新的描述符. 如果fd和fd2是一样的话,那就返回fd2即使打开也不要关闭了. 如果不一样,fd flag中的FD_CLOEXEC将清除,这样的话即使进程调用exec,fd2也是会打开的. 那么dup之后是什么样子的呢?
这里写图片描述
我们来解析一下图上的案例: 我们一开始有fd 1是打开的,然后我们调用dup(1)返回了3, 首先说明0, 1, 2都是打开的,3是目前能用的最小的fd, 这个时候,1和3都指向了同一个file table entry,也就是说说它们共享着同样的file status flag, 比如说是读写O_APPEND等, 它们还共享文件的current file offset, v-node就更不用说了. 当然还有一种方式能进行fd的复制. 就是fcntl我们稍后会详解.
dup(fd);=fcntl(fd, F_DUPFD, 0)
相似的:
dup2(fd, fd2);="close(fd2);fcntl(fd, F_DUPFD, fd2)". 但是这里也稍微有些不同, 你应该也看出来了,fcntl这里进行了两步操作,这就涉及到原子操作的问题, 没错,dup2就是原子操作. 在close和fcntl直接是有可能被信号捕获到的. 当然不同线程改变文件描述符也是可能发生的. 还有一点不同就是它们errno设置值不同.

3.13 sync, fsync, fdatasync

在我们Unix的大部分IO操作中, 都会经过内核中的高速缓冲buffer和页缓存. 也就是是说当我们写数据的时候,内核先拷贝到自己的buffer中,然后再扔到写入队列中之后写入磁盘. 这就叫做延迟写入. 那么kernel什么时候将延迟写入的块儿写入到磁盘呢?Unix为我们提供了三个函数来控制这一过程:
这里写图片描述
sync这个函数就是简单的将数据放到写入队列中后直接返回的,是不会等待磁盘写入操作完成后才返回的. 这个函数呢通常在我们的系统中会被update这个守护进程每隔30秒调用一次, 这就是为了保证内核block缓存区的刷新.
fsync呢函数中也能看出是针对于一个文件描述符的,并且它会等待磁盘写入完成后才返回. 通常用在类似数据库的操作上,保证数据写入磁盘后返回.这个函数是会将文件的属性也会更新同步的,如果发生了改变的话.
fdatasync区别于fsync的就是它只会更新数据部分,即使文件的属性改变,也是不会进行改动的. 在FreeBSD8.0上是不支持fdatasync的.

3.14 fcntl

这里写图片描述
fcntl的主要五大作用:
1. 复制一个存在的描述符(cmd=F_DUPFD或者F_DUPFD_CLOEXEC)
2. 获取或者设置fd flags(cmd=F_GETFD或者F_SETFD)
3. 获取或者设置文件状态flag(cmd=F_GETFL或者F_SETFL)
4. 获取或设置异步IO所有权(cmd=F_GETOWN或者F_SETOWN)
5. 获取或设置记录锁(cmd=F_GETLK或者F_SETLK或者F_SETLKW)
我们就先说前八个,等以后介绍到记录锁的时候再续后三个关于记录锁的cmd:
F_DUPFD: 这个就是返回一个未打开的并且是当前最小的fd, 和之前一样,他们共享了file table entry, 但是他们有着自己的fd flag,也就是说新的fd中的fd flag中的FD_CLOEXEC是清除了的, 这样的话当执行exec函数后,fd还是打开的.
F_DUPFD_CLOEXEC: 同上一样,只不过新的fd的fd flag的FD_CLOEXEC设置了. 注意有点系统支持并不是用FD_CLOEXEC, 而是用0表示don’t close-on-exec, 1表示do close-on-exec.
F_GETFD: 返回fd flag的值.
F_SETFD: 设置fd flag, 具体设置的值是根据第三个参数走.
F_GETFL: 对file tabele entry 中的file status flag的值进行获取:
这里写图片描述
这里需要注意的是前5个标志并不是都只占1位,并且只能拥有这五个中的期中之一,所以必须通过O_ACCMODE来进行取得访问方式位. 然后结果与这五个之一进行比较.
F_SETFL: 通过第三个参数进行设定,但是设定的值不能是前五个. 你想想我都打开文件描述符了,我怎么能设置前五个那种值.
F_GETOWN: 获取当前接收SIGIO和SIGURG信号的进程ID或者进程组ID.
F_SETOWN: 设置接收SIGIO和SIGURG信号的进程ID或进程组ID. arg>0表示是进程ID,arg<0表示绝对值是组ID.
我们用一段代码来感受一下:

int main(int argc, char *argv[])
{
    int val;
    if(argc != 2)
    {
        perror("usage: fcntl <descriptor#>");
    }
    if((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
    {
        perror("fcntl err for fd");
    }
    switch(val & O_ACCMODE)
    {
        case O_RDONLY:
            printf("read_only");
            break;
        case O_WRONLY:
            printf("write only");
            break;
        case O_RDWR:
            printf("read write");
            break;
        default:
            perror("unknown access mode");
    }
    if(val & O_APPEND)
    {
        printf(", append");
    }
    if(val & O_NONBLOCK)
    {
        printf(", nonblock");
    }
    if(val & O_SYNC)
    {
        printf(", synchronous writes");
    }
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
    if(val & O_FSYNC)
    {
        printf(", synchronous writes");
    }
#endif
    putchar('\n');
    return 0;
}

我们首先说一下”_POSIX_C_SOURCE”是什么鬼, 这个其实就是表示支持POSIX的C标准的宏,定义了这个宏的程序就能随意使用POSIX C中的功能了. 具体不同C标准参考C标准简介这篇文章. 然后我们进行结果输出:
这里写图片描述
我们怎么理解这一段测试案例呢?
“>是一种只写的重定向”
“<是一种只读的重定向”
“>>是一种只写,追加的重定向”
“<>是一种读写重定向”
./fcntl x是程序主体,fd是隶属于某个进程的,x等于几,就意味这要检查fcntl这个进程的几号文件描述符。当它的x号fd被重定向以后,fcntl去检查x时,结果就会与重定向的方式有关。
但是当我们在设置不管是file table flag还是fd flag的时候,都不要直接调用F_SETFD或者是F_SETFL去设置, 因为这样的话很有可能将之前设置的标志都冲掉了. 所以我们的做法就是需要调用GET来现获取然后进行添加:

void set_fl(int fd, int flags)
{
    int val;
    if((val = fcntl(fd, F_GETFL, 0)) < 0)
    {
        perror("fcntl F_GETFL err");
    }
    val |= flags;//trun on flags
    if(fcntl(fd, F_SETFL, val) < 0)
    {
        perror("fcntl F_SETFL err");
    }
}

当我们想去除一个标志时:
val &= ~flags
当我们调用的时候:
set_fl(STDOUT_FILENO, O_SYNC);我们来分析一下这个过程:
当我们1号文件描述符设置入O_SYNC后表示这个文件描述符表中的有了同步写入的标志,也就是说不仅仅是普通的write这种只是放入到queue中而不去等待写入磁盘就返回,而是会等待写入磁盘才会返回. 这种情况我们也提到过,多用于数据库的写操作中. 但是对不不同的文件系统中,这个O_SYNC到底会不会生效呢?
这里写图片描述
我们首先在Linux的ext4文件系统中看看, 首先分析第一行为什么要比第二行的system time低很多,是因为第一行只是进行了读操作,而第二行的写操作一般都是会先从磁盘中读取文件然后再写入到另一个磁盘中. 但是我们期望的是有了O_SYNC标志后system time要增加很多,但是从第三四五行来看,并不是这样的. 这说明了Linux的ext4文件系统中并没有给我们O_SYNC的效果,也就是说并不会返回错误,但是也不会给你生效. 我们在看看OS X的HFS文件系统:
这里写图片描述
这就和我们所想的一样了.
所以我们看出每一种调用的性能都由很多因素影响,操作系统的实现,磁盘驱动的速度,文件系统等.

ioctl

这个ioctl就是用户与设备驱动打交道的方式,是通过IO来进行打交道的. 比如说终端I/O就是用这个函数用的最多的. 当我们搞驱动开发的时候,就会用到这个来允许用户层对设备进行一些特殊需求的控制,但是我们在之后会用这个函数去控制终端设备的窗口大小等等这些需求,大家也可以看ioctl详解这篇文章简单的进行了解.


联系方式: reyren179@gmail.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值