系统级I/O入门

一切皆文件。

(本篇中的例子若非特殊说明,使用的都是abcde.txt,文件内容就是abdce)

  • Linux shell创建的每个进程开始时都有三个打开的文件(在头文件<unisted.h>中定义):
    0:标准输入
    1:标准输出
    2:标准错误
  • 文件操作有:open,read,write,stat,dup2,close,lseek;
  • 每个Linux文件都有一个类型:普通文件,目录,套接字等。
    那我们开始看看实践的题目叭!

打开和关闭文件

进程通过open函数来打开一个已经存在的文件或者创建一个新文件

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

int open(char* filename,int flags, mode_t mode);

open函数会返回一个文件描述符,是在当前进程中没有打开的最小描述符
根据我们上面讲到的,shell创建的进程中首先会打开三个文件,所以用户打开的文件的最小进程号应该是3.
open函数带有的flags参数指明了打开的方式:

  • O_RDONLY:只读
  • O_WRONLY: 只写
  • O_RDWR: 可读可写
  • O_CREAT: 文件不存在,就创建一个它的截断的空文件。(就是创建一个新空白文件)
  • O_TRUNC: 如果文件已经存在,就截断它。(清空它)
  • O_APPEND: 在每次写操作前,设置文件位置到文件结尾处。

mode参数指定了访问权限。

  • S_IRUSR:使用者(拥有者)能够读这个文件
  • S_IWUSR:使用者(拥有者)能够写这个文件
  • S_IXUSR:使用者(拥有者)能够执行这个文件
  • S_IRGRP:拥有者所在组的成员能够读这个文件
  • S_IWGRP:拥有者所在组的成员能够写这个文件
  • S_IXGRP:拥有者所在组的成员能够执行这个文件
  • S_IROTH:其他人(任何人)能够读这个文件
  • S_IWOTH:其他人(任何人)能够写这个文件
  • S_IXOTH:其他人(任何人)能够执行这个文件
    这里的mode用一个三位的八进制数进行标识,也就意味着一共九位01二进制来表示是否可读,可写,可执行。

关闭文件

#include <unistd.h>
int close(int fd);

如果尝试关闭已经关闭的文件的话,就会报错。

读写文件

#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
ssize_t write(int fd,const void *buf,size_t n);

read函数从描述符为fd的文件的当前位置复制最多n个字节到位置buf。三种返回值:-1出错,0表示EOF,或者是读取到的字节的个数。
write函数从内存buf出至多复制n个字节到描述符为fd的当前文件位置。

让我们来看个例子

/* $begin cpstdin */
#include "csapp.h"

int main(void) 
{
    char c;

    while(Read(STDIN_FILENO, &c, 1) != 0) 
	Write(STDOUT_FILENO, &c, 1);
    exit(0);
}

STDIN_FILENO就是标准输入,STDOUT_FILENO就是标准输出,SITERR_FILENO就是标准错误
运行结果:
在这里插入图片描述
我们看到这其实就是一个循环读入,每次从标准输入就是键盘当中输入一个字符的时候,就自动读取一个字符到缓冲区中去,直到读到一个\n就输出所有缓冲区的内容到标准输出就是屏幕上面。

读取文件元数据

调用stat或者fstat函数

#include <unistd.h>
#include <sys/stat.h>

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

stat函数以一个文件名作为输入,可以查看到文件的各种数据。我们可以用man stat 看一下Linux里关于它的一些信息。
在这里插入图片描述
保存信息的stat结构体的结构
在这里插入图片描述
然后让我们来试着看一下这个代码

/* $begin statcheck */
#include "csapp.h"

int main (int argc, char **argv) 
{
    struct stat stat;
    char *type, *readok;

    /* $end statcheck */
    if (argc != 2) {
	fprintf(stderr, "usage: %s <filename>\n", argv[0]);
	exit(0);
    }
    /* $begin statcheck */
    Stat(argv[1], &stat);
    if (S_ISREG(stat.st_mode))     /* Determine file type */
	type = "regular";
    else if (S_ISDIR(stat.st_mode))
	type = "directory";
    else 
	type = "other";
    if ((stat.st_mode & S_IRUSR)) /* Check read access */
	readok = "yes";
    else
	readok = "no";

    printf("type: %s, read: %s\n", type, readok);
    exit(0);
}

运行结果:
在这里插入图片描述
这一个程序能够查看文件类型是普通文件,目录,还是其他。以及这个文件能不能被当前用户读。Linux在sys/stat.h中定义了宏谓词来确定st-mode成员的文件类型:
S_ISREG(m)是否为普通文件
S_ISDIR(m)是否为目录文件
S_ISSOCK(m)是否为网络套接字
这里的st_mode成员就是我们上面所说的那个无符号八进制数,让它和S_IRUSR做与运算,就能得到它是否可读。

共享文件

内核用三个相关的数据结构来表示打开的文件

  • 描述符表
    每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。每个描述符表项只想文件表中的一个表项。
  • 文件表
    打开文件的集合是由一张文件表来表示的,所有进程共享。它记录了当前文件的位置,当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。当引用计数为0是,内核会自动删除这个文件表表项。
  • v-node表。所有进程共享,包含了stat结构中的大多数信息。

    这里就是打开了两个不同的文件,fd1和fd4通过不同的文件表表项A和B来引用两个不同的文件,这里没有共享文件,并且每个描述符对应一个不同的文件。
    在这里插入图片描述
    上面这一个就是两次打开了同一个文件。关键思想就是每个描述符都有他自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。简单点想就是只要是打开文件不进行后续的dup或者dup2,文件描述符都对应不同的文件表表项。
    然后回到我们之前讲的fork,fork一次,生成了一个新的子进程,子进程复制了所有父进程的环境。那么如果父进程中打开了文件的话,就有如下的情况:
    在这里插入图片描述
    我们先来看看这个程序
#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;
}

运行结果:
在这里插入图片描述
我们看到了:
1.先打开了abcde.txt,并且读取了一个字符,现在光标停留在了ab之间
2.进行了fork,子进程复制了父进程的环境。
3.然后两个进程都遇到了sleep,接着先继续执行子进程,它读了一个字符,因为先前光标停留在ab之间,所以此时c2读到的是b
4.child执行完后回到父进程,父进程和子进程的文件表表项相同,所以光标在b后,往后读一个,读到了c
所以最后的输出结果就是如图所示辣。
讲完这个,我们来讲

I/O重定向

I/O重定向能帮助用户将磁盘文件和标准输入输出联系起来。
举个栗子

linux>ls>foo.txt

使得shell加载和执行ls程序,将标准输出重定向到磁盘文件foo.txt
那么如何重定向呢?
一种考的是dup2函数

#include <unistd.h>
int dup2(int oldfd,int newfd);

若成功则为非负的描述符,若出错则为-1
dup2函数做的其实就是用oldfd的文件表表项替换掉newfd的文件表表项,此外如果newfd是打开的状态的话,会需要先关闭掉newfd。示意图如下:
在这里插入图片描述
这里的操作就是dup2(4,1),这个例子里面内核会自动销毁文件A的文件表和v-node表项,然后B的引用计数会加一。
看个例子:

#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;
}

运行结果:
在这里插入图片描述
解释一下,首先fd1,fd2,fd3拥有不同的文件表表项,但指向同一个v-node。接着,dup2了一下.此时,fd2和fd3指向了相同的文件表表项都是fd2的文件表表项。然后读入c1,fd1中的光标位标记为ab之间,fd2的仍然是a前。然后读入c2,此时fd2的光标也在ab之间了,接着读入c3的时候,fd2和fd3是同一个文件表表项,所以c3读走了b。所以结果如图所示。
接着我们看看dup。(呜呜,要是早点写就好了。期末碰到了这一题,但是好像也写对了,嘿嘿)

#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
*/

这里我们看到了一个新的函数dup。

int dup(int oldfd);

dup和dup2的区别,其实简单讲就是dup2把前一个文件项覆盖了后一个文件项,而dup就是直接再建一个文件,文件的文件项就是oldfd。
来看看这道题的意思:
首先打开给的文件,如果没有就造一个,如果有就清空它。
然后在这个文件里写"pqrs"四个字符
紧接着fd3也打开了这个文件,打开的方式是光标停留在文本文件的最后一个字符后面。然后写了"jklmn"五个字符。所以现在文件里的样子是"pqrsjklmn"
紧接着dup了一下,所以现在fd2中应该是"pqrsjklmn",光标停留在pqrs后面,因为fd1的光标也在那里。
然后写fd2,此时写的"wxyz"显然会覆盖"jklm"
最后又写fd3,注意到fd3的光标在最末尾,所以ef写在了最后
所以文件最后就变成了这样:
在这里插入图片描述

本博客部分照片来自于csapp课本上解说的图片。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值