六星经典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个 ...
随机推荐
- Web SCADA 电力接线图工控组态编辑器
前言 SVG并非仅仅是一种图像格式, 由于它是一种基于XML的语言,也就意味着它继承了XML的跨平台性和可扩展性,从而在图形可重用性上迈出了一大步.如SVG可以内嵌于其他的XML文档中,而SVG文档中 ...
- windows下 在cmd中输入ls命令出现“ls不是内部或外部命令“解决方法
1.新建一个文件命名为 ls.bat 2.打开编辑这个文件 输入: @echo off dir 3.将这个文件放在C:\windows目录下
- json pickle ;shelve
import json dic={'name':'alex'} """ f=open("new_hello","w") # dic ...
- 使用springcloud zuul构建接口网关
一 微服务网关背景及简介 不同的微服务一般有不同的网络地址,而外部的客户端可能需要调用多个服务的接口才能完成一个业务需求.比如一个电影购票的收集APP,可能回调用电影分类微服务,用户微服务,支付微服 ...
- 从三个开源项目认识OpenFlow交换机 - OVS
在SDN/NFV的网络革新技术浪潮的引领下,催生了诸多数据面开源方案的诞生.业界知名度较高的有OVS(Open vSwitch).FD.io (Fast Data I/O).ODP(Open Data ...
- java制作验证码(java验证码小程序)
手动制作java的验证码 Web应用验证码的组成: (1)输入框 (2)显示验证码的图片 验证码的制作流程: 生成验证码的容器使用 j2ee的servlet 生成图片需要的类: (1) Buffere ...
- Redis常用命令--Lists
List是一个双向链表,按照插入顺序排序,可以添加一个元素到头部或者尾部.当对一个空key执行插入操作的时候会创建一个新表. 如果要清空列表,则会杀出对应的key空间. 在List中保存了头节点和未节 ...
- Shiro整合Spring
首先需要添加shiro的spring整合包. 要想在WEB应用中整合Spring和Shiro的话,首先需要添加一个由spring代理的过滤器如下: <!-- The filter-name ma ...
- 51nod 1514 美妙的序列
Description 长度为n的排列,且满足从中间任意位置划分为两个非空数列后,左边的最大值>右边的最小值.问这样的排列有多少个%998244353 题面 Solution 正难则反 \(f[ ...
- HDU 5506(GT and set)
题意: 表示看了很久,然而发现还是没看懂题. 正解:给你a个集合,让你把他们合并成k个,当两个集合有公共数字时可以合并. (一直以为是合并后,每个集合至少有两个数字相同- -,这英语也是醉了) 思路: ...