上篇文章:Linux:基础IO
目录
书接上文
1.系统级别调用
1.1得到与open函数r模式相同的效果
使用的函数:

int main(int argc, char *argv[])
{
if(argc != 2)
{
printf("Usage: %s filename\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
if(fd < 0)
{
perror("open");
return 2;
}
char inbuffer[128];
while(1)
{
ssize_t n = read(fd, inbuffer, sizeof(inbuffer)-1);
if(n > 0)
{
inbuffer[n] = 0;
printf("%s", inbuffer);
}
else if(n == 0)
{
printf("end of file!\n");
break;
}
else
{
perror("read");
break;
}
}
close(fd);
return 0;
}
运行结果:

2.文件描述符
2.1认识fd
通过对open函数的学习,我们知道了文件描述符就是一个小整数:
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fd: %d\n", fd);
return 0;
}
运行结果:

在一个进程中,可以打开多个文件,那打开多个文件时,fd代表哪些数字呢?
int main()
{
int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fda : %d\n", fda);
printf("fdb : %d\n", fdb);
printf("fdc : %d\n", fdc);
close(fda);
close(fdb);
close(fdc);
return 0;
}
结果:

从3开始的原因
因为在创建程序/进程时,程序/进程会先创建标准输入输出流。(stdin:标准输入,键盘;stdout:标准输出,显示器;stderr:标准错误,显示器)

这里的FILE在c语言中是一种结构体,而这个FILE结构封装了fd,
验证:
printf("stdin: %d\n", stdin->_fileno);
printf("stdout:%d\n", stdout->_fileno);
printf("stderr:%d\n", stderr->_fileno);

所以操作系统在系统层面,只认文件描述符。
为什么是连续的
推测:0123是它的数组下标
为什么它可以表示文件
文件被分为属性和内容,当你要访问磁盘时本质是将文件加载到内存中。那么打开文件的本质就是预加载文件,而一个进程可以打开多个文件,同时操作系统内部需要对多个文件进行管理。再通过下述图片的链接,操作系统对被打开的文件进行建模,转换为对链表的增删查改。

进程通过struct files_struct对被打开的文件进行管理:

文件描述符表的数组下标指向文件的数据,通过文件描述符可以对文件进行管理。
fd的本质
本质就是数组下标
printf("stdin: %d\n", stdin->_fileno);
printf("stdout:%d\n", stdout->_fileno);
printf("stderr:%d\n", stderr->_fileno);
FILE *fp = fopen("log.txt", "w");
printf("log.txt: %d\n", fp->_fileno);

2.2文件描述符
2.2.1源码验证


2.2.2文件描述符分配规则

从最小的没有被使用的下标开始分配,分配下标需要再数组中连续:
close(0);
int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fda : %d\n", fda);
printf("fdb : %d\n", fdb);
printf("fdc : %d\n", fdc);

close(2);
int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fda : %d\n", fda);
printf("fdb : %d\n", fdb);
printf("fdc : %d\n", fdc);

但是当关闭数组下标1时:
close(1);
int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fda : %d\n", fda);
printf("fdb : %d\n", fdb);
printf("fdc : %d\n", fdc);
显示器中没有显示,在loga.txt中倒是有显示。此时输出重定向:

C函数封装和系统调用,fd之间的关系
只能用fd,只能是系统调用,而C++中,std::cout;std::cin;std::cerr这些类,也必定要封装fd。
像Python解释器,Java的虚拟机都是用C/C++完成的。所以也必定封装fd.
那么这些语言为什么要做封装?直接系统调用不行吗?
以C语言为例,此时他已经被封装好了。
那么在C语言在Linux中安装,它需要调用Linux系统调用,在Windows中安装,需要调用Windows系统调用。即使这两个系统调用不同,单用户只需要学习C语言封装的接口即可。底层的差异由C标准库解决。达到跨平台性!
语言为什么要跨平台性?一种平台的背后有着一大批开发者,最终为了争取开发者。
跨平台性的本质是打造语言的生态。
2.2.3理解重定向
为什么当关闭数组下标1时:
close(1);
int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fda : %d\n", fda);
printf("fdb : %d\n", fdb);
printf("fdc : %d\n", fdc);
显示器中没有显示,在loga.txt中倒是有显示。此时输出重定向:

原因:printf实际是通过stdout,而它对应的int fd = 1;
close(1);
int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
fprintf(stdout, "fda : %d\n", fda);
fprintf(stdout, "fdb : %d\n", fdb);
fprintf(stdout, "fdc : %d\n", fdc);
printf("fda : %d\n", fda);
printf("fdb : %d\n", fdb);
printf("fdc : %d\n", fdc);

重定向本质就是1号下标指向的标准输出根据文件描述符的分配规则被改变为指向新打开的文件:

dup2专门进行重定向
重定向是将目标文件中文件描述符中的内容拷贝到被重定向的文件描述符1里面,类似于发生一种浅拷贝。
进行重定向都需要先关闭文件再打开文件,dup2 makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:

示例:
int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
dup2(fda, 1);
printf("hello printf\n");
fprintf(stdout, "helo fprintf\n");

它的效果与echo相似:

输入重定向

int fda = open("loga.txt", O_RDONLY);
dup2(fda, 0);
int a = 0;
float f = 0.0f;
char c = 0;
scanf("%d %f %c", &a, &f, &c);
printf("%d, %f, %c\n", a, f, c);

此种操作相当于:

3.优化自主shell命令行解释器

优化Execute函数:
void Execute()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if(id == 0)
{
// child
// 程序替换
//
// 重定向
if(redir_type == IN_REDIR)
{
int fd = open(redir_filename, O_RDONLY);
(void)fd;
dup2(fd, 0);
}
else if(redir_type == OUT_REDIR)
{
int fd = open(redir_filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
(void)fd;
dup2(fd, 1);
}
else if(redir_type == APP_REDIR)
{
int fd = open(redir_filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
(void)fd;
dup2(fd,1);
}
else
{
// nothing
}
execvp(argv[0], argv);
exit(1);
}
else{
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
(void)rid;
lastcode = WEXITSTATUS(status);
}
}
新增Redir函数:
void Redir()
{
// 核心目标 "ls -a -l >> > < filename"
char *start = commandline;
char *end = commandline+strlen(commandline);
while(start < end)
{
// > >> <
if(*start == '>')
{
if(*(start+1) == '>')
{
// 追加重定向
// ls -a -l >> filename
redir_type = APP_REDIR;
*start = '\0';
start += 2;
CLEAR_LEFT_SPACE(start);
redir_filename = start;
break;
}
else
{
// 输出重定向
redir_type = OUT_REDIR;
*start = '\0';
start++;
CLEAR_LEFT_SPACE(start);
redir_filename = start;
break;
}
}
else if(*start == '<')
{
// 输入重定向
redir_type = IN_REDIR;
*start = '\0';
start++;
CLEAR_LEFT_SPACE(start);
redir_filename = start;
break;
}
else
{
start++;
}
}
}
4.拓展
每一个进程都会打开标准输入,标准输出,标准错误,0,1,2
为什么要打开?为什么是他们?怎么做到的?
程序的本质是加工处理用户数据的,而程序要有办法获得数据,一般主要获得数据的途径为键盘获取和显示器打印,支持debug,0,1,stdin,stdout
为什么要有stderr?
Linux/Unix 程序默认有两个输出通道,完全独立:
- 标准输出 stdout(文件描述符 1) 放正常运行的普通信息 ,
printf、cout默认往这里输出。 - 标准错误 stderr(文件描述符 2) 放报错、警告、异常信息 ,
perror、cerr默认往这里输出。
这两个流本来是分开的,只是默认都打印在终端,看起来混在一起。

运行结果:

实际上,直接使用 > 是一种简写,本质是:

是怎么默认打开三项标准的?
当一个进程打开若干文件,如果这个进程创建子进程,那么当前进程的文件描述符表要给子进程拷贝一份(浅拷贝,指针部分也要拷贝),所以子进程从来没有打开过三项标准,都是从bash父进程继承过来的。所以会默认打开。
5.理解"一切皆文件"
在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket这样的东西,使用的接口跟文件接口也是一致的。
这样做最明显的好处是,开发者仅需要使用一套API和开发工具,即可调取Linux系统中绝大部分的资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。
之前我们讲过,当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体, 该结构体定义在 /usr / src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linu×/fs.h 下, 以下展示了该结构部分我们关系的内容:
struct file {
...
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
...
atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指
向它,就会增加f_count的值。
unsigned int f_flags; // 表⽰打开⽂件的权限
fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所
有的标志在头⽂件<fcntl.h> 中定义
loff_t f_pos; // 表⽰当前读写⽂件的位置
...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK
*/
值得关注的是 struct file 中的 f_op 指针指向了一个 file_operations 结构体,这个结构体中的成员除了struct module*owner 其余都是函数指针。 该结构和 struct file 都在fs.h下。
struct file_operations {
struct module *owner;
//指向拥有该模块的指针;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
"signed size" 类型, 常常是⽬标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负,
返回值代表成功写的字节数.
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned
long, loff_t);
//初始化设备上的⼀个异步写.
int (*readdir) (struct file *, void *, filldir_t);
//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤
返回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打开⼀个⽂件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
int (*release) (struct inode *, struct file *);
//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
int (*fsync) (struct file *, struct dentry *, int datasync);
//⽤⼾调⽤来刷新任何挂着的数据.
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
//lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实
现它.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t
*, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
file_operation就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。


上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是"linux下一切皆文件"的核心理解。
本章完。