1、补充系统调用
上一篇文章中已经介绍了打开还有关闭的系统调用,但是还有一些的调用没有讲到,现在就简单的讲解一下吧。
1、1、read调用
系统调用也包括了read的选项。
从指定文件描述符描述的文件中读取,读取到buffer中,buffer期望读取多少呢?根据count个字节来判断。
1、2、stat
stat的系统调用的作用就是类似于status。能够获取指定文件的属性。
图上的介绍就是,stat调用能够通过路径或者是文件描述符来获取到struct stat的结构体。 struct stat这又是什么呢?那肯定是关于文件属性的结构体啊,我们man 2 stat的时候能够看到。
其中这结构体中的变量都代表一个文件的各种属性,换句话说全部属性也都在这里了。 并且这个结构体在这个函数中是一个输出型参数,当这个函数调用结束之后,能够根据所指定的文件的属性来填充其中的内容。 上面的系统调用参数想要试用的话也很简单,根据上面的参数需求,下面来演示一下。
2、重定向
2、1、文件描述符的分配规则
进程会查询自己的文件描述表,分配最小的没有被使用的fd(没有实现的难度,就相当于是遍历数组,找最小下表)。
2、2、实现重定向(dup2)
如果我们知道了上面的文件描述符的分配规则的情况下的话,如果我们关闭了fd=1的情况,并且在下面printf或者是fprintf的时候还是照样的是一个默认,另一个指向的还是stdout的话(加上fflush),此时展现的结果就是会创建一个文件,并且在文件中有着我们原本需要打印的内容,那这不就是换一种意义上的重定向的含义吗。代码如图
其中C语言中的这两个函数其实指向本质应该是这样:fprintf/printf->stdout->struct FILE->stdout->_fileno==1。所以底层的变化,关close(1)的时候,在上层上的C语言函数中,不能够直接看出变化,所以在上层调用的时候就还是会照样的继续去访问原本的fd==1的下标,然后继续在其中写入的话,就会在我们创建的文件中开始写入原本向现实其中显示的内容,这样不就是展示出了重定向的操作了。 这样看来的话,重定向在上层没有需要操作的,需要改变的就只有OS内部fd文件操作符的变化。 可是当我们不加上fflush的话,写不进去,这又是为什么呢?缓冲区和这个又有什么关系呢? 其实是因为在C语言级别的stdin,stdout以及stderr的FILE结构体中,也包含了语言层面上的文件缓冲区,我们刚刚介绍的文件操作函数也并不是直接通过系统调用直接存放在OS中的内核文件缓冲区中,而是存放在了语言层面上的缓冲区中,也就是说,如果不刷新的话,只会存在语言缓冲区中,不会真正的写到文件内核缓冲区中,也就不会在进程结束的时候,展示出我们新写的文件。所以我们要fflush让语言层面的缓冲层刷新到系统层面上然后对文件进行写或者别的操作。 这么看的话,重定向是不是有点粗糙,每次的重定向都还需要关闭文件,然后再打开才能够实现重定向吗? 当然不是了!介绍系统调用接口dup2。
要知道dup2不只是两个整数之间的拷贝,而是文件下标是代表的文件内容的拷贝。当然哪一个是old哪一个是new就需要看函数解释( 我就简单的搜一下这个调用的介绍,想要更深入的了解到话,还是自行搜搜吧)
看完函数解释知道newfd是oldfd的拷贝,所以要重定向的话,最后留下来的是oldfd的,所以重定向位置的文件的操作符是oldfd,原本指向显示器的fd= =1的文件操作符拷贝了oldfd的文件操作符。所以应该选2。
这样的话原本指向1的显示器的文件操作符的话,现在就是指向了oldfd。虽然这样的话可能会导致多个文件操作符指向同一个文件,但是不用担心,因为在struct file中会存在int ref_count,类似于引用计数。当然,原本有被指向的文件可能在ref_count == 0的时候就会自行进行关闭。
3、缓冲区的理解
缓冲区不只存在一个地方,首先得知道缓冲区有内核级缓冲区也还有用户级缓冲区。但是所有的缓冲区,都有两个效果。首先就是解耦,还有一个就是提高效率 举个例子来简单化的效果的话就像是,我们平时见到的超市一样,我们需要的时候直接去超市里面拿就可以了,我们不回去跑那么远,还要去工厂中去拿,所以这种方便也就是像缓冲区一样给系统的速度增加,同时这种情况也减少了程序之间的耦合性质,我们不需要管到底怎么样才能将我们的数据存到数据之上,我们只需要把我们的数据按照键盘输入就行了。就像是,我们不需要管到底怎么样把商品弄到超市,我们只需要在超市拿就行了。 可是,我们进行数据的拷贝到缓冲区的时候难道不是会增加时间吗?怎么会减少时间的呢?我们直接把数据拷贝到文件缓冲区中这样不应该是更快吗,为什么要多花一次时间拷贝到语言层面上的缓冲区呢? 其实不然,这里的提高效率是提高谁的效率?是提高商品和超市的效率吗?不是的,提高的是客户和商品之间的效率,换句话说也就是提高使用者的效率,使用者之需要交到缓冲区就可以,并不需要等待OS进行交换数据的时间,这样的话,使用者就能够继续使用,这样的话,就能够提高使用者使用的效率。 除此之外还不建议多次的调用系统调用,因为系统调用也是需要消耗时间的,所以放在语言层的缓冲区,就会积攒一些之后然后再开始刷新缓冲区,启动系统调用,这是提高IO刷新的效率。 再说一下,其实对于语言来说,并不是一定要做到给我们提供缓冲区的,但是提供缓冲区的话,就能够大大的方便操作者使用的效率了,所以说,这也就是为什么C语言在当时,包括现在,流行了那么多年的原因,随着我们学习的深入,我们也逐渐理解到C语言中为我们使用者考虑到的细节,优化使用者的使用感受,也提高了系统的效率。 缓冲区的实现方式也有挺多种可能的,就比如说,只要有数据就使用fflush,刷新到OS的缓冲区,还有一种就是行刷新,现实就是这么刷新的,还有一种就是全缓冲,一般对应的就是普通文件的刷新,只有当缓冲区都写满的时候才会进行刷新。当然这都是在进程正常进行的时候才会有的状态,如果进程停止了的话,系统会自动刷新。 还有就是我们能够强制进程中的缓冲区直接刷新,最简单的例子就是使用fflush。值得注意的是这是在Linux操作系统上的缓冲区刷新规则,如果在windows上的话可能还会有所不一样
3、1、缓冲区典型实例
下面请看前提的代码
这样的代码下,如果我们不加上fork的话,我们最后实现的是什么呢?
实现出来是这样的,如果我们不是指向显示器的情况呢?反而是指向文件的重定向操作呢?
这样的结果反而是这样的,那如果说,我们加上fork这样的创造出来的子进程的话,又会是什么样子呢?
出现的反而是这个样子。这究竟是怎么一回事呢?答案就是问题出现在缓冲区刷新的定义不同导致的。 对于显示器来说,是一行的刷新,但是对于文件来说是全缓冲的刷新方式。利用了重定向,也就导致了刷新策略的不同,这样的话就导致了结果的不一样。还有就是可以看出区别就是,打印两遍的几乎都是库函数,只有系统调用的才打了一次。 知道不一样了之后再来细致的分析一下。其实对于fork来说,复制的是父进程的几乎所有的信息,所以在对于重定向到文件中的可运行程序来说,调用C语言实现的文件操作还没有刷新到OS中的缓冲区,还是在语言层面上的缓冲区中,所以对于子进程来说还会复制其中存留的消息,所以在最后fork完了结束之后的时候,才会显示出两遍的C语言函数的打印。
3、2、缓冲区代码形式展示
简单截图一下,这就是我们C语言中的FILE结构体,其中也包含之前所讲到的_fileno的标志符号。中间的一大部分也是为了维护语言级别的缓冲区而存在的大量的定义。这样的话,我就能够实现C语言对于库函数调用然后实现数据的格式化形式再交给系统,让系统进行操作下面的后续问题。
4、深化和实践利用
4、1、在shell中加入重定向
实现重定向,就需要我们能够读取得到重定向的方式,在此之前的shell中我们能够读取到我们用户的输入的指令。想要读到重定向的话也就特别简单,因为重定向的操作符也就只有三个,所以遍历一遍的话也就能够找到重定向的操作符了。当然了,找到重定向的操作符之后,需要我们去寻找后面的文件名,此时也需要注意,跳过合适的空格,找到真正的文件名称。类似于我们的那个之前shell中就有的寻找cwd名字时候的宏函数操作,这里的宏函数的优点也是显而易见的,1、能够直接使用函数内临时变量不需要进行传址操作,更能够让人读懂。2、宏函数在处理的时候会直接替换,这样的话能够减少变量创建,缩短一点时间。
代码语言:javascript复制void CheckRedir(char cmd[])
{
// > >> <
// "ls -a -l -n > myfile.txt"
int pos = 0;
int end = strlen(cmd);
while(pos < end)
{
if(cmd[pos] == '>')
{
if(cmd[pos 1] == '>')
{
cmd[pos ] = 0;
pos ;
redir_type = App_Redir;
SkipSpace(cmd, pos);
filename = cmd pos;
}
else
{
cmd[pos ] = 0;
redir_type = Out_Redir;
SkipSpace(cmd, pos);
filename = cmd pos;
}
}
else if(cmd[pos] == '<')
{
cmd[pos ] = 0;
redir_type = In_Redir;
SkipSpace(cmd, pos);
filename = cmd pos;
}
else
{
pos ;
}
}
}
这样的操作就是找到重定向符号的种类,同时确定重定向后面的文件的名字,能够帮助我们找到对应的文件。这里我们在找到符号之后就把那个位置设置为0,是因为在后一步的SplitCommand中,需要找到的结尾也是和0相同的ASCII码的值,这样能够稍微写的优雅一点。 还需要修改的地方就是执行我们命令的时候。
代码语言:javascript复制void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
//重定向设置
if(filename != NULL){
if(redir_type == In_Redir)
{
int fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir_type == Out_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir_type == App_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{}
}
// child
execvp(gArgv[0], gArgv);
exit(errno);
}
else
{
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%dn", gArgv[0], strerror(lastcode), lastcode);
}
}
}
需要进行重定向的设置,通过调用dup2的系统调用,实现我们上层能够看到的文件的重定向的操作,关于dup2的函数的参数的设置,还有功能,已经在刚刚讲过了,可以回头看看复习一下。 这样的话,我们就能够通过自己实现的shell实现读取操作,也能够实现重定向操作了。
4、2、简单实现库的封装
这一步的简单说明主要还是介绍,缓冲区的设置和定义的,并没有别的需求上的条件。所以主要关注缓冲区在其中的意义就行了。 实现库的封装实际上是挺难的,所以为了防止这么多了的情况下,还要讲不少的内容。所以期待下一篇吧!