系统级I/O

系统级I/O

输入/输出(I/O)是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。

一:Unix I/O

一个Linux文件就是一个m个字节的序列: B0,B1,…,Bk, …,Bm- 1

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

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

二:文件

1)文件类型
每个Linux文件都有一个类型(type)来表明它在系统中的角色:

  1. 普通文件(regular file)包含任意数据。应用程序常常要区分文本文件(text file)和二进文件(binary file),文本文件是只含有ASCII或Unicode字符的普通文件;二进制文件是所有其他的文件。对内核而言,文本文件和二进制文件没有区别。
  2. 目录(directory)是包含一组链接(link)的文件,其中每个链接都将一个文件名(filename)映射到一个文件,这个文件可能是另一个目录。每个目录至少含有两个条目:“.”是到该目录自身的链接,以及“…”是到目录层次结构(见下文)中父目录(parent directory)的链接。你可以用mkdir命令创建一个目录, 用 ls 查看其内容,用rmdir删除该目录。
  3. 套接字(socket)是用来与另一个进程进行跨网络通信的文件,其他类型略。

Linux内核将所有文件都组织成一个目录层次结构(diretory hierarchy),由名为/的根目录确定。系统中每个文件都是根目录的直接或间接的后代。下图显示了Linux系统的目录层次结构的一部分。

在这里插入图片描述
可以用cd命令来修改shell中的当前工作目录。
2)路径
目录结构层次中的位置用路径名来指定。有两种形式:
1.绝对路径名:以一个斜杠开始,表示从根节点开始的路径。如上图的hello.c的绝对路径名为:/home/droh/hello.c。
2.相对路径名:以文件名开始,表示从当前工作目录开始的路径。上图中:如果/home/droh是当前的工作目录,那么hello.c的相对路径名就是./hello.c;反之,如果/home/bryant是当前的工作目录,那么相对路径名就是…/home/droh/hello.c。

3)打开和关闭文件
进程通过调用open函数来打开一个已存在的文件或者创建一个新文件的:

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

int open(char *filename, int flags, mode_t mode);
		            //返回:若成功则为新文件描述符,若出错为-1。

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:

  • O_RDONLY:只读。
  • O_WRONLY:只写。
  • O_RDWR:可读可写。
    例如下面的代码说明如何以读的方式打开一个已存在的文件:
    fd = Open("f00.tet‘’, O_RDONLY, 0);

flags参数也可以是一个或者更多位掩码的或,为写提供一些额外的指示:

  • O_CREAT:如果文件不存在,就创建它的一个截断的(truncated)(空)文件。
  • O_TRUNC:如果文件已经存在,就截断它。
  • O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。
    例如下面的代码说明如何打开一个已存在的文件,并在后面添加一些数据:
    fd = Open(“foo.txt”, O_WRONLY|O_APPEND,0);

mode参数指定了新文件的访问权限。这些位的符号名字如下图:
在这里插入图片描述
作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置为mode&~umask。
接下来,创建一个新文件,拥有者有读写权限,其他用户有读权限:
umask(DEF_UMASK);
fd = Open(“foo.txt”, O_CREAT|O_TRUNC|O_WRONLY,DEFF_MODE);

最后,进程通过调用close函数关闭一个打开的文件。

#include <unistd.h>

int close(int fd);
		//返回:若成功则为0,若出错则为-1。

关闭一个已关闭的描述符会出错。
4)读和写文件:
应用程序是通过分别调用read和write函数来执行输入和输出的。

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
  //返回:若成功则为读的字节数,若EOF则为0,若出错则为-1。
ssize_t write(int fd, const void *buf, size_t n);
   //返回:若成功则为写的自己数,若出错则为-1。 

read函数从描述符为fd的当前文件位置最多赋值n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则表示实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。下图是一个实例

#include "csapp.h"

int main(void){
		char c;
		while(Read(STDIN_FILENO,&c,1)!=0)
				Write(STDOUT_FILENO, &C,1);
		exit(0);
}

某下情况会有不足值不表示一定有错误,可能如下情况:
1.读时遇到EOF。
2.从终端读文本行。如果打开的是与终端相关的(如键盘和显示器),那么每个read函数将一次传送一个文本行,不足值为文本行大小。
3.读和写网络套接字。

dup2 函数复制描述符表表项 oldfd 到描述符表表项 newfd ,覆盖描述符表表项 newfd 以前的内容。如果 newfd 已经打开了, dup2 会在复制 oldfd 之前关闭 newfd。

int dup2(int oldfd, int newfd);

5)读取文件元数据
应用程序能够通过调用stat和fstat 函数,检索到关于文件的信息(有时也称为文件的元数据(metadata))。

int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);

st_ size 成员包含了文件的字节数大小。st_ mode成员则编码了文件访问许可位和文件类型(10.2节)。Linux 在sys/stat.h中定义了宏谓词来确定st mode 成员的文件类型:

S_ISREG(m)。这是一个普通文件吗?
S_ISDIR(m)。这是一个目录文件吗?
S_ISSOCK(m)。这是一个网络套接字吗?
6)共享文件
描述符表(descriptor table)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
文件表(file table)。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成(针对我们的目的)包括当前的文件位置、引用计数(reference count)(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
v-node表(v-node table)。同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st mode 和st_ size成员。

三:程序实例

1.hello.c

#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("h");
    printf("e");
    printf("l");
    printf("l");
    printf("o");
    printf("\n");
    fflush(stdout);
    exit(0);
}

打印出hello;

在这里插入图片描述
2.

#include "csapp.h"

int main(int argc, char *argv[])
{
    int fd1, fd2, fd3;
    char c1, c2, c3;
    char *fname = argv[1];
    fd1 = open(fname, O_RDONLY, 0);
    fd2 = open(fname, O_RDONLY, 0);
    fd3 = open(fname, O_RDONLY, 0);
    dup2(fd2, fd3);

    read(fd1, &c1, 1);
    read(fd2, &c2, 1);
    read(fd3, &c3, 1);
    printf("c1 = %c, c2 = %c, c3 = %c\n", c1, c2, c3);

    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}


输出:c1=a, c2=a, c3=b
分析:首先三个描述符fd来打开abcde.txt(内容是 abcde),dup2复制描述符表表项fd2到fd3,因此,fd3会去读第二个字节,三个read函数分别读取fd1,fd2,fd3即文件abcde.txt中一个字节。最后的结果是c1=a, c2=a, c3=b。
3.

#include "csapp.h"

int main(int argc, char *argv[])
{
    int fd1;
    int s = getpid() & 0x1;
    char c1, c2;
    char *fname = argv[1];
    fd1 = open(fname, O_RDONLY, 0);
    read(fd1, &c1, 1);
    if (fork()) {
	/* Parent */
	sleep(s);
	read(fd1, &c2, 1);
	printf("Parent: c1 = %c, c2 = %c\n", c1, c2);
    } else {
	/* Child */
	sleep(1-s);
	read(fd1, &c2, 1);
	printf("Child: c1 = %c, c2 = %c\n", c1, c2);
    }
    return 0;
}

输出:Parent: c1=a, c2=b
Child: c1=a, c2=c
分析:只有一个文件描述符fd1,c1为abcde,txt中的第一个字节a;接下来,fork函数生成子进程,父进程执行if语句:s= getpid() & 0x1,这里则为1(getpid为正数),即休眠0.001s,执行read函数,c2为abcde,txt中的第二个字节b;子进程执行else语句:s= getpid() & 0x1,这里则为0(getpid为0),c2为abcde,txt中的第二个字节b后面的一个字节c;
因此有上述输出结果。
4.

#include "csapp.h"

int main(int argc, char *argv[])
{
    int fd1, fd2, fd3;
    char *fname = argv[1];
    fd1 = open(fname, O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR);
    write(fd1, "pqrs", 4);	

    fd3 = open(fname, O_APPEND|O_WRONLY, 0);
    write(fd3, "jklmn", 5);
    fd2 = dup(fd1);  /* Allocates new descriptor */
    write(fd2, "wxyz", 4);
    write(fd3, "ef", 2);

    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}

/*abcde.txt
pqrswxyzef
*/

在这里插入图片描述
输出如上图
分析:fd1以O_CREAT|O_TRUNC|O_RDWR创建并打开了一个可读可写的文件,如果文件已存在则将其内容清空;fd3则是可以在文件末尾对文件内容进行添加,以只写的方式打开文件。
起初文件中内容为abcde,但是fd1打开它的时候就清空了然后往文件里面写数据pqrs。
fd3打开文件,在后面加上jklmn,此时文件中内容为pqrsjklmn,fd1的位置上在j处,fd3位置在文件末尾。
用dup函数,让fd2成为fd1的副本,所以fd2的位置也在j处,从j处开始写入数据wxyz,将jklm覆盖,留下一个n,最后再在文件末尾添加ef,得到结果pqrswxyznef。
(!!!此处分析引用自「Zero零诺斯」
版权声明:本文为CSDN博主「Zero零诺斯」的原创文章,
原文链接:https://blog.csdn.net/weixin_44632375/article/details/103333793)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值