系统调用I/O(文件I/O)

文件描述符

  • 通过open函数打开一个文件,可以得到一个存储该文件所有属性的结构体(类似于标准io的FILE),但是又不完全像标准io,系统io的open会将这个结构体的指针存放到一个数组(文件描述符表)中,然后返回这个指针在数组中的下标,而是一个int值(不像fopen返回一个FILE*),即fd。之后就通过操作这个fd来操作文件。
  • 这个数组下标0、1、2的位置分别一般来说都存放着由系统保留的stdin、stdout、stderr
  • 文件描述符在允许使用范围内优先使用数组中下标最小的

具体可以看文件共享一节内容

IO操作

open close

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

参数flags必须包含下面三个中的一个: O_RDONLY, O_WRONLY, O_RDWR。另外,0或多个文件创建选项和文件状态选项可以以按位或(|)的形式加入到flags中,常用的有以下几个:

  • O_CREAT 文件不存在时会创建
  • O_TRUNC 文件存在且用只写或读写打开,则将文件截断为0长度
  • O_EXCL 如果文件存在则报错,用来测试文件存不存在。一般常和O_CREAT一起用,不存在就创建
  • O_APPEND 写入时在文件尾追加
  • O_NONBLOCK 以非阻塞的方式打开,如果需要等待就不排队,过后再打开。阻塞地打开就是排队等待直到打开为止

如果用标准io的fopen里的打开方式来比拟的话:
r -> O_RDONLY
r+ -> O_RDWR
w -> O_WRONLY | O_CREAT | O_TRUNC
w+ -> O_RDWR | O_CREAT | O_TRUNC
a -> O_APPEND | O_CREAT
a+ -> O_RDWR | O_APPEND | O_CREAT

int close(int fd);

返回值

open(), openat(), and creat() 返回新的文件描述符,失败就返回-1

read write

ssize_t read(int fd, void *buf, size_t count);

从fd中读count个内容放入buf中

ssize_t write(int fd, const void *buf, size_t count);

从buf中取出count个内容写入fd。注意这里的buf是const的,因为这里的buf是取内容的,我们不能更改它,而read要更改buf

返回值

read成功会返回读到的字节数,读到文件尾就返回0,如果失败就返回-1,并且设置errno
write成功会返回成功写入的字节个数,如果返回0表示什么都没写进去,返回-1表示出错。

使用样例

int fds = open(argv[1], O_RDONLY);
if (fds < 0)
{
	perror("open()");
    exit(1);
}
int fdd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fdd < 0)
{
	close(fds);
	perror("open()");
	exit(1);
}

char buf[BUFSIZE];
int pos;
long int len = 0L, ret = 0L;
while (1)
{
	len = read(fds, buf, BUFSIZE);
    if (len < 0)
    {
    	perror("read()");
        break;
    }
    else if (len == 0)
     	break;
    //防止出现没有把len个字节全部写入的情况
    pos = 0;
    while (len > 0)
    {
    	ret = write(fdd, buf + pos, len);
        if (ret < 0)
        {
        	perror("write()");
            exit(1);
        }
        pos += ret;
        len -= ret;
     }
}
close(fdd);
close(fds);

lseek

off_t lseek(int fd, off_t offset, int whence);

和fseek使用无二,不过lseek返回的是当前文件内容指针当前指向的位置,失败则返回-1

系统IO和标准IO的区别

系统IO每次调用都从用户切换到内核态,实时性高。标准IO有缓冲机制,每次的输入输出实际都放入缓冲区中。所以系统IO的响应速度快,标准IO靠缓冲区先堆积,再一次性传输,所以吞吐量大。

int fileno(FILE *stream);
//该函数将FILE指针转换为一个文件描述符fd
//实现标准IO的操作变成系统IO的操作

FILE *fdopen(int fd, const char *mode);
//将一个系统IO打开的文件描述符封装到FILE*中使用

注意:标准IO和系统IO不要混用!

文件共享

内核使用3种数据结构表示打开文件:

  1. 文件描述符 。每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,每个描述符都有一个文件描述符标志和指向一个文件表项的指针。
  2. 文件表项 。内核为所有打开文件维持一张打开文件表,每个文件表项包含
    1 ) 文件状态标志(读,写,添加,同步和非阻塞等)
    2 ) 当前文件偏移量
    3 ) 指向该文件 v 节点表项的指针
  3. v节点结构 ,包含
    1 ) 文件类型
    2 ) 对此文件进行各种操作的函数的指针
    3 ) 文件的 i 节点。i 节点包含文件的所有者,文件长度,文件所在的设备,指向文件实际数据块在磁盘上所在位置的指针等等。 不过Linux 没有使用 v 节点,而是使用了通用 i 节点结构。

先是最常见,最普通的场景,两个进程都打开不同的文件,相互之间还没有共享
在这里插入图片描述
然后是一个进程两次打开同一个文件
在这里插入图片描述
这里可以看到虽然每个fd都有自己独立的文件表项,但实际都是在操作同一个文件。之所以每个fd都有自己的文件表项,就是为了可以使每个fd都有它自己的对该文件的当前偏移量。所以,一个fd的偏移改变不会影响另一个fd的偏移。
最后是两个进程打开同一文件的场景
在这里插入图片描述
可以看到,两个进程也是有各自的文件表项。除了跨进程外和上面同一进程两次操作同一文件没有区别。这里着重考察一个具体场景,就是两个进程同时打开文件进行追加(O_APPEND)写。假设蓝色进程(PA)写入一些数据完成后,它的 offset 会被更新,如果这个值大于 inode 中的文件 size,则更新 inode.size 到 offset 表示文件增长了;然后 PB 开始写入数据,由于指定了 O_APPEND 标志位,在写入前,系统会先将它的文件表项中的 offset 更新为当前 inode.size,这样就可以得到 PA 写入后的文件末尾位置,接着在这个位置写入 PB 的数据,写入完成后的逻辑与 PA 相同,会更新 offset、inode.size 来表示文件的最新增长。由于更新 offset 与 inode.size 是在一个函数中完成的,所以这个操作完全可以被某种锁保护起来,从而实现原子性。相对的,如果没有指定 O_APPEND 选项,而使用 lseek (fd, 0, SEEK_END) + write (fd, buf, size) 的写法,由于这个操作需要使用两个函数来完成,无法跨函数加锁使得这样的操作没有原子性保证,而可能产生的竞争会导致一个进程写入的数据被另一个进程所覆盖,从而丢失数据。

接下来两个函数可以实现在一个进程中让两个文件描述符指向同一个文件

dup dup2

int dup(int oldfd);

像下图这样,dup会拷贝数组中oldfd的指针,然后放到数组可用范围中下标最小的且没用过的位置,然后返回这个新的文件描述符。成功后两个fd都会指向同一个文件表现,如果失败返回-1
在这里插入图片描述

使用样例

现在想不在标准输出(默认是屏幕)上进行输出,而是输出到指定的文件。标准输出的fd默认是1,所以如果close(1),把显示器的文件关掉,然后dup自己文件的fd,就可以让这个fd拷贝到下标最小的地方也就是1,实现了标准输出到自己的文件。

int main(int argc, char *argv[])
{
    int fd = open("./test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0600);
    if (fd < 0)
    {
        perror("open()");
        exit(1);
    }

    close(1); //a
    int fd2 = dup(fd); //b
    close(fd); //现在fd2和fd都指向test.txt,可以fd
    //fd2在1号位,而系统默认往1号位的文件输出
    //所以现在输出到test.txt中
    puts("helloworld");

    exit(0);
}

这里有两个隐藏的大问题,我们这里默认了0、1、2都是被stdin out err占用的,但并不是一定占用,假设1没有被占用,我们上面的代码就会出现问题。如果1本身就是空的,则我们open文件后的fd就是1,再看代码中注释a的地方,这里释放了1处的文件,而b处代码又会拷贝1处已经被释放的指针,甚至后面还要close(fd)…这是第一个问题
第二个问题,考虑并发的情况。假设我们执行了a处然后时间片到了,切换别的进程,而这个进程还创建了一个文件,直接顶替了我们close的1号。再换回我们的进程,此时dup之后就不会拷贝到我们想要的1号。换句话说,代码中a和b的操作不原子。
使用dup2来解决上述问题:

int dup2(int oldfd, int newfd);
//dup2会先关掉newfd的文件,然后复制oldfd的指针到newfd处

可以看出这个dup2相当于代码中close和dup共同使用的原子版,解决了第二个问题,那第一个问题呢?实际dup2有两个要点:

  • If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
  • If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.

第二个要点可以看出,如果newfd和oldfd一样,dup2将什么都不做。这却导致了新的问题,代码中我们最后会把fd关掉,但如果两个fd一样dup2什么都不做,我们关掉一个使得其中一个fd变成空悬指针…所以加个判断:

dup2(fd, 1);
if (fd != 1) //防止fd本身就是1的情况
	close(fd);
puts("helloworld");

同步:sync fsync fdatasync

其他

fcntl ioctl

/dev/fd/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值