六星经典CSAPP-笔记(10)系统IO
六星经典CSAPP-笔记(10)系统I/O
1.Unix I/O
所有语言的运行时系统都提供了高抽象层次的I/O操作函数。例如,ANSI C在标准I/O库中提供了诸如printf和scanf等I/O缓冲功能的函数;C++中则重载了<<和>>用来支持读写。在Unix系统中,这些高层次的函数基于Unix的系统I/O函数来实现,多数时候我们都无需直接使用底层的Unix I/O。但学习Unix系统I/O能更好地理解一些系统概念,而且当高层次的函数不适用时我们也能轻松地实现想要的功能,例如访问文件的元数据。
Unix/Linux将各种I/O设备统一而优雅地抽象为文件,在此基础上提供了一套非常简洁的低层次的API接口,也就是Unix I/O。这套API主要由以下五个函数组成:
- open():请求内核打开一个file,从而应用代码能够访问file对应的I/O设备。内核会返回一个非负整数,叫做描述符descriptor。内核会为打开的file维护一组数据结构(学习Linux内核时了解了一些),可以把描述符想成file在内核中的id,后续各种操作都需要它。
- lseek():修改当前file的操作位置k,即从file开端的字节偏移量。读写文件时会自动移动k,也可以显式调用lseek()移动k。
- read():从位置k开始,从file拷贝大于0个字节到内存中。当k超过文件的总字节大小时,会触发一个条件
end-of-file(EOF)
。 - write():类似地,从内存拷贝大于0个字节到file的位置k处,并更新k。
- close():通知内核释放file在内核中对应的数据结构,将描述符放回到资源池中。进程结束时,内核会自动关闭所有打开的file。
EOF到底是什么东西?
首先必须明确一点,没有EOF字符这种东西。EOF是一种状态或条件,由操作系统内核去检测是否达到这种条件。当应用程序从read()中读取到0时,就说明达到EOF条件了。对于磁盘file,EOF表示位置k超过了文件大小。对于网络连接file,EOF表示连接的一端关闭了连接,另一端就会检测到EOF。
2.打开关闭文件
下面详细学习一下open()和close()函数:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/** Returns: new file descriptor if OK, −1 on error */
int open(char *filename, int flags, mode_t mode);
#include <unistd.h>
/** Returns: zero if OK, −1 on error */
int close(int fd);
flags参数指明想要怎样访问文件,它可以由几个模式组合而成:
- O_RDONLY:只读
- O_WRONLY:只写
- O_RDWR:读写
- O_CREAT:文件不存在则创建
- O_TRUNC:文件存在则truncate它
- O_APPEND:写文件前先将位置k置为末尾,即追加模式
mode参数指明新建文件的访问权限,此参数可以省略。要注意的是:每个进程都有一个由umask()函数设置的umask
,open()函数最终创建出的文件的访问权限是mode & ~umask
:
- S_IRUSR/S_IWUSR/S_IXUSR:Owner的读、写、执行权限
- S_IRGRP/S_IWGRP/S_IXGRP:Owner所在组中成员的读、写、执行权限
- S_IROTH/S_IWOTH/S_IXOTH:其他任何用户的读、写、执行权限
3.读写数据
read()和write()至多拷贝n个字节,返回值:-1表示错误,0表示EOF,大于0则表示实际拷贝的字节数。
#include <unistd.h>
/** Returns: number of bytes read if OK, 0 on EOF, −1 on error */
ssize_t read(int fd, void *buf, size_t n);
/** Returns: number of bytes written if OK, −1 on error */
ssize_t write(int fd, const void *buf, size_t n);
ssize_t和size_t有什么区别?
size_t被定义为unsigned int,而ssize_t被定义为int。因为read()和write()要返回-1表示发生错误,所以要使用ssize_t作为返回值。
#include <unistd.h>
int main(int argc, char const *argv[])
{
char c;
while(read(STDIN_FILENO, &c, 1) != 0) {
write(STDOUT_FILENO, &c, 1);
}
return 0;
}
在以下几种情况下,read()和write()可能会返回少于n个字节:
- 没有数据可读了:假如我们以50字节chunk读取,而只有20个字节可读,调用read()就会遇到EOF。
- 从终端读取:每次只能读取到终端上的一行文本。
- 读/写Socket:Socket内部的缓冲限制和网络延迟会导致只能读取一部分数据。同理,进程间的Unix管道pipe、IPC等读写的情况也类似。
而当你从硬盘读取数据时,除非读到末尾触发EOF,你永远也不会碰到返回小于n字节的情况。同样写数据时也从不会碰到这种情况。
4.文件元数据
可以通过文件名或描述符获得文件的元数据。
#include <unistd.h>
#include <sys/stat.h>
/** Returns: 0 if OK, −1 on error */
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
/* Metadata returned by the stat and fstat functions */
struct stat {
dev_t st_dev; /* Device */
ino_t st_ino; /* inode */
mode_t st_mode; /* Protection and file type */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device type (if inode device) */
off_t st_size; /* Total size, in bytes */
unsigned long st_blksize; /* Blocksize for filesystem I/O */
unsigned long st_blocks; /* Number of blocks allocated */
time_t st_atime; /* Time of last access */
time_t st_mtime; /* Time of last modification */
time_t st_ctime; /* Time of last change */
};
例如,下面是一个结合了文件打开关闭和元数据读取的小例子,首先新建文件,然后利用fstat()函数查看新创建文件的访问权限,并以类似ls命令的格式打印出来:
// printf, getchar
#include <stdio.h>
// malloc
#include <stdlib.h>
// open, mode_t
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
// close
#include <unistd.h>
mode_t getumask();
char *mode2str(mode_t mode);
int main(int argc, char const *argv[])
{
int fd;
struct stat stat;
/*
* 1.Print default umask.
* The typical default value for the process umask is:
* S_IWGRP | S_IWOTH (octal 022)
*/
printf("umask: %s\n", mode2str(getumask()));
// 2.Create a new file
if ((fd = open("foo.txt",
O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR)) == -1) {
fprintf(stderr, "Create file failed\n");
exit(1);
}
// 3.Check if new file mode = mode & ~mask
if (fstat(fd, &stat) == 0)
printf("file mode: %s\n", mode2str(stat.st_mode));
else
printf("Get metadata failed\n");
// 4.Close file
close(fd);
return 0;
}
/**
* There is a getumask(), but only specified in glib_c
* , not portable.
* @return current umask
*/
mode_t getumask()
{
mode_t mode = umask(0);
umask(mode);
return mode;
}
/**
* Acted like 'ls -l'
* @param mode file mode
* @return human-read
*/
char *mode2str(mode_t mode)
{
char *str = malloc(10 * sizeof(char));
int i = 0;
str[i++] = S_ISREG(mode) ? 'F' : 'D';
str[i++] = mode & S_IRUSR ? 'R' : '-';
str[i++] = mode & S_IWUSR ? 'W' : '-';
str[i++] = mode & S_IXUSR ? 'X' : '-';
str[i++] = mode & S_IRGRP ? 'R' : '-';
str[i++] = mode & S_IWGRP ? 'W' : '-';
str[i++] = mode & S_IXGRP ? 'X' : '-';
str[i++] = mode & S_IROTH ? 'R' : '-';
str[i++] = mode & S_IRWXO ? 'W' : '-';
str[i++] = mode & S_IXOTH ? 'X' : '-';
return str;
}
5.文件共享
Unix有很多种共享文件的方式,在学习之前先要了解Unix内核为每个文件维护了哪些数据结构。正是有这些数据结构的存在,才产生了多种多样的共享方式:
- 描述符表(descriptor table):每个进程都有一张独立的描述符表,每个entry中都保存了打开file的描述符和指向文件表的指针。
- 文件表(file table):文件表是所有进程共享的,每个entry保存了当前位置,指向文件的描述符的个数(可能有多个)和指向v-node表的指针。当引用个数减少为0时,内核会删除掉该文件表项。
- v-node表(v-node table):v-node表也是所有进程共享的,每个entry保存了元数据stat的信息。
下面就看一下最常见的三种情况。
5.1 不共享
5.2 同一文件被打开多次
5.3 子进程继承父进程已打开文件
下面是一个小例子,open_twice()尝试重复打开一个文件两次,然后用两次得到的描述符从文件中读取一个字符,结果读到的是’f’,说明v-node表项相同,但file table未发生共享。而inherit_parent_fd()则是打开文件后,fork一个子进程,子进程先读取一个字符,父进程等子进程结束后再读取一个字符,结果读到的是’o’,说明file table发生共享,子进程的读取导致位置k发生偏移。
// printf
#include <stdio.h>
// exit
#include <stdlib.h>
// open, seek
#include <fcntl.h>
// read, write
#include <unistd.h>
// wait
#include <wait.h>
void open_twice(char *filename);
void inherit_parent_fd(char *filename);
int main(int argc, char const *argv[])
{
open_twice("foo.txt");
inherit_parent_fd("foo.txt");
return 0;
}
void open_twice(char *filename)
{
int fd1, fd2;
char c;
fd1 = open(filename, O_RDONLY);
fd2 = open(filename, O_RDONLY);
read(fd1, &c, 1);
read(fd2, &c, 1);
printf("c = %c\n", c);
}
void inherit_parent_fd(char *filename)
{
int fd;
char c;
fd = open(filename, O_RDONLY);
if (fork() == 0) {
read(fd, &c, 1);
exit(0);
}
wait(NULL);
read(fd, &c, 1);
printf("c = %c\n", c);
}
6.标准I/O
6.1 标准I/O库
ANSI C定义了一组更多层次的I/O函数,叫做标准I/O库(libc),提供了Unix C外的另一种选择:
- 打开关闭:fopen()和fclose()
- 读写字节:fread()和fwrite()
- 读写字符:fgets()和fputs()
- 格式化I/O:scanf()和printf()
标准I/O库将打开的file抽象为stream。对于程序员来说,stream就是指向FILE类型的指针。stream或者说FILE类型,其实= file描述符 + buffer,其目的就是在内部维护一块缓冲区,从而避免频繁调用开销很大的系统调用。此外,每个ANSI C程序启动时都会自动打开三个stream,对应标准输入、输出、错误流:
#include <stdio.h>
extern FILE *stdin; /* Standard input (descriptor 0) */
extern FILE *stdout; /* Standard output (descriptor 1) */
extern FILE *stderr; /* Standard error (descriptor 2) */
大多数C程序员在其整个职业生涯中都只使用标准I/O库,而不会直接使用底层的Unix I/O,这也是推荐的做法。但标准I/O库在处理网络全双工通信时会有些限制:
- 限制1:调用output函数后,要调用fflush,fseek,fsetpos或rewind后,才能调用input函数。因为对Socket使用lseek是非法的,所以可以在每次调用input前,先调用fflush重置读取位置能够解决此问题。
- 限制2:调用input函数后,要调用fseek,fsetpos或rewind,才能调用output函数,否则会遇到EOF。此问题只能为读和写分别打开两个FILE来解决。
6.2 I/O重定向
Unix Shell提供了I/O重定向操作符,例如unix> ls > foo.txt。这个功能是由dup2()函数来完成的。dup2会拷贝oldfd的描述符表entry覆盖到newfd的entry,假如之前newfd是打开状态的,那么dup2会先关闭newfd再开始拷贝。例如,默认stdout对应fd1,假设foo.txt对应fd4,那么dup2(4, 1)会将fd4的描述符表entry覆盖到fd1,所以最终stdout会和fd4一样,都指向foo.txt文件的file table表项。
#include <unistd.h>
/** Returns: nonnegative descriptor if OK, −1 on error */
int dup2(int oldfd, int newfd);
六星经典CSAPP-笔记(10)系统IO的更多相关文章
- 六星经典CSAPP笔记系列 - 作者:西代零零发
六星经典CSAPP笔记(1)计算机系统巡游 六星经典CSAPP笔记(2)信息的操作和表示 六星经典CSAPP-笔记(3)程序的机器级表示
- 六星经典CSAPP笔记(1)计算机系统巡游
CSAPP即<Computer System: A Programmer Perspective>的简称,中文名为<深入理解计算机系统>.相信很多程序员都拜读过,之前买的旧版没 ...
- 六星经典CSAPP笔记(2)信息的操作和表示
2.Representing and Manipulating Information 本章从二进制.字长.字节序,一直讲到布尔代数.位运算,最后无符号.有符号整数.浮点数的表示和运算.诚然有些地方的 ...
- 六星经典CSAPP-笔记(7)加载与链接(上)
六星经典CSAPP-笔记(7)加载与链接 1.对象文件(Object File) 1.1 文件类型 对象文件有三种形式: 可重定位对象文件(Relocatable object file):包含二进制 ...
- 六星经典CSAPP-笔记(11)网络编程
六星经典CSAPP-笔记(11)网络编程 参照<深入理解计算机系统>简单学习了下Unix/Linux的网络编程基础知识,进一步深入学习Linux网络编程和TCP/IP协议还得参考Steve ...
- 六星经典CSAPP-笔记(12)并发编程(上)
六星经典CSAPP-笔记(12)并发编程(上) 1.并发(Concurrency) 我们经常在不知不觉间就说到或使用并发,但从未深入思考并发.我们经常能"遇见"并发,因为并发不仅仅 ...
- Java nio 笔记:系统IO、缓冲区、流IO、socket通道
一.Java IO 和 系统 IO 不匹配 在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚.操作系统并非不能快速传送数据,让 Java 有事可做:相反,是 JVM 自身在 I/O 方面 ...
- 基础笔记10(IO 1.7try-with-resource) 装饰模式
1.读写的类型分为字节流和字符流,字节流一般是视频音频其他所有的类型都可以. (非文档文件使用字符流易造成未知编码(?)错误) InputStream OutputStream 抽象类 fileInp ...
- 六星经典CSAPP-笔记(3)程序的机器级表示
1.前言 IA32机器码以及汇编代码都与原始的C代码有很大不同,因为一些状态对于C程序员来说是隐藏的.例如包含下一条要执行代码的内存位置的程序指针(program counter or PC)以及8个 ...
随机推荐
- CSS3 3D transform变换
.实际应用-图片的旋转木马效果 您可以狠狠地点击这里:图片的旋转木马效果demo 建议在足够新版本的FireFox浏览器或Safari浏览器下观看,Chrome可能需要居中定位查看,下图为效果缩略图: ...
- Android基础字符串String.md
问题抛出 String这个常量在我们代码中会经常被用到,那么我们了解 String stringbuffer StringBudilder三者之间的区别吗 问题解答 String 字符串常量,位于常量 ...
- [LeetCode] Maximum Average Subarray II 子数组的最大平均值之二
Given an array consisting of n integers, find the contiguous subarray whose length is greater than o ...
- [LeetCode] Design Log Storage System 设计日志存储系统
You are given several logs that each log contains a unique id and timestamp. Timestamp is a string t ...
- 迎圣诞,拿大奖活动赛题_SQLi(sprintf格式化字符)
看题目应该就是注入没跑了 先拿正常输入试试,输了admin,admin,结果返回password error!,说明还真的存在admin账户 拿常规注入语句试探一下admin' and 1=1%23, ...
- 个人建站&mac下安装hexo
title: 个人建站&mac下安装hexo date: 2018-04-18 16:34:02 tags: [mac,blog,个人建站,markdown] --- 这两天使用了markdo ...
- Python 装饰器示例
#!/usr/bin/env python # _*_ coding: UTF-8 _*_ # Author:taoke def applePrice(func): def otherfunc(): ...
- ●BZOJ 3561 DZY Loves Math VI
题链: http://www.lydsy.com/JudgeOnline/problem.php?id=3561 题解: 莫比乌斯反演 $$\begin{aligned}ANS&=\sum_{ ...
- ●BZOJ 4516 [Sdoi2016]生成魔咒
题链: http://www.lydsy.com/JudgeOnline/problem.php?id=4516 题解: 把串反过来后,问题变为求每个后缀的互不相同的子串个数.首先用倍增算法求出 sa ...
- bzoj4830 hnoi2017 抛硬币
题目描述 小 A 和小 B 是一对好朋友,他们经常一起愉快的玩耍.最近小 B 沉迷于**师手游,天天刷本,根本无心搞学习.但是已经入坑了几个月,却一次都没有抽到 SSR,让他非常怀疑人生.勤勉的小 A ...