标准I/O和系统I/O
这是如今的I/O图示:我们现在大部分都在使用系统I/O以及标准I/O,当然不同情况下需求也不同!
- 系统I/O是最低层的,最接触硬件的,因为是操作系统提供的
- 标准I/O则是因为不同操作系统的系统调用不一样而应运而生,C说:我可以搞定~ 因此C中的一系列调用都是可以跨操作系统的,因此被称为标准I/O
系统级I/O
- 输入输出(I/O)是在主存和外部设备(如磁盘驱动器,终端和网络)之间复制数据的过程。
一切皆文件:在Linux系统中经常用的一句话,在这将会得到深入理解。
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单的应用接口:Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
- 打开文件:一个应用程序通过要求内核打开相应的文件,来表明它想要访问一个I/O设备。这时内核会返回一个小的非负整数,叫做文件描述符(它是关键!它返回的是当前所能用的最小数):用来标识这个文件。
- Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符为0),标准输出(1),标准错误(2)
- 改变当前文件的位置:对于每个打开的文件,内核保持着一个文件位置K,初始为0,应用程序能通过执行seek操作,显示的设置K
- 读写文件:一个读操作就是从文件复制n>0个字节到内存,从K位置开始,当超过时会触发一个称为EOF的条件。
- 关闭文件:当应用完成对文件的访问之后,就通知内核关闭这个文件。作为相应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
文件
每个Linux文件都有一个类型来表明它在系统中的角色,主要有:
- 普通文件(regular file):包含任意数据,如文本文件和二进制文件(但这哥俩在内核眼中没有区别,都是0101组成的)
- 目录(directory):是包含一组Link的文件,每个Link都将一个文件名映射到一个文件。
Linux内核将所有文件都组织成一个目录层次结构,由根目录:/ 确定。
所有用户对应主目录都在home下!如:~: /home/cy
接下来就通过一些代码实验来了解一下系统I/O叭~
实验一
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
我们先来看看read的原型:
ssize_t read(int fd, void *buf, size_t n):
- fd :文件描述符,你到底读哪个文件
- buf: 缓冲区,从文件中读到内存位置buf
- n:读的字节数
成功则返回实际传送的字节数量,失败则返回-1,返回0则表示EOF,即文件读完。
了解完函数就可以看出,这个就是一个死循环,STDIN_FILENO就是标准输入设备,也就是你的键盘啦,每输入一个字符,就显示一个字符到显示屏上
这里Ctrl+C 强制退出即可。
实验二
#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;
}
假设打开文件的内容为abcde
这个程序打开的是同一文件,分别用不同的文件描述符3,4,5来描述,接下来出现了dup2函数,这个函数其实就是用fd2覆盖fd3,其实就是现在打开fd3其实指向的是fd2喔。看下图,一开始如果读fd1,fd2,fd3是这样的
用了dup2之后,则变成这样
最重要的是光标会停在上回打开时的位置!!
接下来我们继续看代码,Read读fd1,读出一个字符a,读fd2,因为是重新打开,所以读出的还是a,之后读fd3,因为现在指向fd2,且刚才fd2已经读出一个字符,现在光标的位置在后面,因此,此次读出的字符为b,所以输出的是c1=a,c2=a,c3=b
实验三
#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;
}
这个实验则是和fork结合起来了,首先定义了一个秒数,用pid号与1,则结果为0或1,之后先打开fd1,读出了一个字符a,之后fork,有两种可能
- s=0:则父进程先执行,子进程睡1s,于是输出
Parent: c1 = a, c2 =b
Child: c1 = a, c2 = c - s=1,则子进程先执行,父进程睡1s,于是输出
Child: c1 = a, c2 = b
Parent: c1 = a, c2 =c
实验四
#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;
}
这个是关于写文件的,打开fd1,向里面写入pqrs,打开fd3,向里面写入jklmn,之后用fd1覆盖fd2,再写其实是向fd1写,但因为此时fd1中光标在s后面,所以新写入的把后面的jklm覆盖掉,之后再打开fd3,此时光标在n后面,写入ef,因此最后的文件内容为pqrswxyznef
实验五
#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);
}
这个是判断文件类型的一个实验,如果是普通文件,类型就为regular,如果是目录文件,类型就为directory,其他则为other,在看用户读的权限,可读为yes,不可读为no,之后输出即可。
好啦,实验就到这里啦,撒花~