CSAPP–第十章–系统级IO(上)

Aki 发布于 2023-01-19 205 次阅读


Unix IO、

  • 所有的输入和输出都能以一种统一且一致的方式来执行:
  • 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O 设备
    • 内核返回一个小的非负整数,叫做文件描述符,它在后续对此文件的所有操作中标识这个文件。
    • 内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  • Linux shell 创建的每个进程开始时都有三个打开的文件:
    • 标准输入(描述符为 0)
    • 标准输出(描述符为 1)
    • 标准错误(描述符为 2)
    • 头文件< unistd.h> 定义了常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_ETLENO,它们可用来代替显式的描述符值。
  • 改变当前的文件位置
    • 对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操作,显式地设置文件的当前位置为k。
  • 读写文件
    • 一个读操作就是从文件复制 n>0 个字节到内存,从当前文件k位置是开始,然后将k增加到k+n。
    • 给定一个大小为m字节的文件,当k>=m时执行读操作,会触发EOF(end-of-file)条件——能被应用程序检测到
    • 文件末尾没有明确的EOF符号,EOF只是c/c++标准库中的一个宏定义,它的值是-1
  • 关闭文件
    • 当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
    • 无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Uniex下一些常见的文件类型、

  • 普通文件(regular file):应用程序常常要区分文本文件(text file)和二进制文件(binary file):
    • 文本文件是只含有 ASCII 或 Unicode 字符的普通文件;
    • 二进制文件是所有其他的文件
    • 对内核而言,文本文件和二进制文件没有区别。
    • 使用 ls -l 命令后,第一列第一个字符为 "-" 的文件为普通文件(使用-F后没有特殊标记)
  • 目录(directory)
    • 包含一组链接(link )的文件,其中每个链接都将一个文件名(filename)映射到一个文件,这个文件可能是另一个目录。
    • 每个目录至少含有两个条目
      • . 表示当前目录
      • .. 表示上一级目录
    • 可以用 mkdir 命令创建一个目录,用 Is 查看其内容,用 rmdir 删除该目录。
  • 套接字(socket)
    • 用来与另一个进程进行跨网络通信的文件
  • Linux 内核将所有文件都组织成一个目录层次结构(directory hierarchy),最大的是根(/)目录。

打开和关闭文件、

进程是通过调用 open() 函数来打开一个已存在的文件或者创建一个新文件的。

特别注意,下面这些系统调用级别的函数都是有返回值的,出现错误会返回一个负数值。一个良好的习惯是在使用这些函数时,需要判断返回值!!!!

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

//原始函数,不安全
int open(const char* path,int flag);

//自己封装过的安全函数
int Open(const char* path, int flag)
{
	int fd = 0;
	if ((fd = open(path, flag)) < 0)
	{
		unix_error("open file error\n");
	}
	return fd;
}

open函数参数一是文件的路径,参数二是打开方式,有只读模式,只写模式,读写模式等。linux定义了一些宏来表示,这些宏本质上是int值。

O_RDONLY(0) 只读打开         O_WRONLY(1) 只写打开         O_RDWR(2)  可读可写打开

文件也是一种系统资源,linux规定了一个程序默认情况下只能打开64个文件,使用完文件不关闭文件会造成资源浪费,所有要及时关闭文件。

进程通过close函数来关闭文件,释放资源。参数是一个打开的文件标识符。

int close(int fd);

void Close(int fd)
{
	int res = 0;
	if ((res = close(fd)) < 0)
	{

               unix_error("close file error\n");
	}
}

读和写文件、

应用程序是通过分别调用 read 和 write 函数来执行输入和输出。

read 函数从文件描述符为 fd 的当前文件位置复制最多n个字节到内存位置 buf。

int read(int fd,const char* buf,size_t n);

//安全的read函数
void Read(int fd,const char* buf, size_t len)
{
	size_t left = len;
	size_t reads = 0;
	char* tmp = buf;

	while (left > 0)
	{
		//error                                                                                            
		if ((reads = read(fd, buf, left)) < 0)
		{
			unix_error("read file error\n");
		}
		//EOF                                                                                              
		else if (reads == 0)
		{
			break;
		}
		left -= reads;
		tmp += reads;
	}
}

write 函数从内存位置 buf 复制至多 len个字节到描述符 fd 的当前文件位置

int write(int fd,const char*buf,size_t len);

//安全的write函数
void Write(int fd,const char* buf, size_t len)
{
	int n = 0;
	if ((n = write(fd, buf, len)) < 0)
	{
		unix_error("write file error\n");
	}
}

在某些情况下,read 和 write 传送的字节比应用程序要求的要少(不足值问题),有下列原因:

  • 读时遇到 EOF。
    • 读完了。
  • 从终端读文本行。
    • 如果打开文件是与终端相关联的(如键盘和显示器),那么每个read 函数将一次传送一个文本行,返回的不足值等于文本行的大小。
  • 读和写网络套接字(socket)。
    • 内部缓冲约束和较长的网络延迟会引起 read 和 write 返回不足值。

在x86-64中,size_t被定义为unsigned long,而 ssize_t被定义为long。

上述总结、

上述提到使用的函数都是低级的I/O函数,有安全隐患或者其他的不足。所以现代的程序会有一些已经编写好了的库,这些库帮我们解决了这些问题,且调用起来和原始的函数相同。