3-文件描述符和标准文件

1. 文件描述符

  在linux系统中有这么一句话:“一切皆文件”,而这些文件又分为:普通文件、目录文件、符号链接文件和设备文件等。对于内核来说,所有打开的文件都通过文件描述符(file descriptor)来进行管理引用,文件描述符是一个非负整数,当打开一个文件或新建一个文件时,内核会向进程返回一个文件描述符,当所有对文件进行读写操作的read/write系统调用都是通过该文件描述符来实现的。

  一般来说,一个进程刚启动时就默认打开了3个文件描述符,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3,因为前三个默认被系统占用,POSIX标准要求每次打开文件时必须使用当前进程中最小可用的文件描述符。

标准文件描述符图如下:

这里写图片描述

  这打开的三个文件分别是:/dev/stdin(标准输入文件),/dev/stdout(标准输出文件),/dev/stderr(标准出错文件)。

2. stdin标准输入文件

  /dev/stdin:是一个标准文件,当程序开始运行时,默认会调用open(“/dev/stdin” , O_RDONLY)打开这个标准文件,返回的文件描述符是0,使用0这个文件描述符就可以从标准输入设备(键盘,鼠标),换句话说,这个标准文件/dev/stdin表示了键盘。

  那么对于read(0 , buf , sizeof(buf))实现的就是从键盘中读取数据,然后存放到缓存buf中,数据的中专过程如下:
这里写图片描述

  也就是说,从键盘读取的数据会先缓存到键盘驱动程序的缓存中,然后再把数据缓存到/dev/stdin标准文件开辟的内核缓存中,然后read函数从/dev/stdin(标准文件)的内核缓存中读取数据,存放到buf缓存中。同时也说明了,在linux中应用程序通过系统调用操作底层硬件时,如键盘,又或者向显示器输出文字时,都是以操作文件的形式来实现的,这也印证了在linux系统中“一切皆文件”的说法。

3. 浅谈scanf函数

   对于C标准的scanf函数相信大家经常使用吧,当使用scanf函数从键盘录入数据时,默认就打开了显示器/dev/stdin(标准文件),文件描述符0指向/dev/stdin,scanf底层就是调用了read函数,然后把文件描述符0当做参数传入read函数来读取数据,就可以从键盘读取了。也就是说,在读取数据时也可以通过read(0)来实现,也可以通过scanf函数来实现,但是一般来说,我们在编写应用程序是还是使用scanf函数,调用库函数可以让程序具有良好的可移植性,兼容不同的系统平台。

  另外,scanf函数相对于read函数来说,更加方便调用者使用,比如格式转换问题。如果直接调用read函数,所有从键盘输入的都是字符,比如:从键盘输入100,对于read函数来说,其实输入的是三个字符:‘1’,‘0’,‘0’,也就是说,read函数从键盘读取的数据都是字符,如果想得到数字100,必须得自己进行格式转换。而scanf函数则弥补了这个缺点,虽然scanf1函数底层还是使用了read函数,还是读取到字符数据,但是scanf函数可以通过%d,%s,%f来指定数据的转换格式,scanf函数会自动将字符形式的数据,转换为整型或浮点型数据。比如:scanf(“%d” , &a)就是将从键盘中录入的字符数据10转换为整型数字10。

read函数读取数字100的例子

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int ret = 0;    
    char buf[30] = {0};
    //ret = scanf("%s", buf);
    //通过read函数从键盘读取数据
    ret = read(0, (void *)buf, 30);
    if(-1 == ret)
    {
        //perror("scanf fail");
        perror("read fail");
        return -1;
    }
    printf("buf = %s\n", buf);  
    int num = 0;
    //由于buf中的100是字符数据,想要得到数字100,就必须将buf中的每个字符'1','0','0'进行转换成对应的数字1,0,0,的ASCII码值
    num = (buf[0] - '0')*100 + (buf[1] - '0')*10 + (buf[2] - '0');
    printf("num = %d\n", num);
    return 0;
}

4. stdout标准输出文件

  对于/dev/stdout标准输出文件来说,程序一运行时,默认通过open (“/dev/stdout”, O_WRONLY)将其打开,返回的文件描述符为1。因为/dev/stdin标准输入文件先打开,文件描述符为0。此时进程的最小可用文件描述符就是1了,当打开/dev/stdout时,就会返回文件描述符1。

  当我们调用write函数,传入1这个文件描述符,就可以将数据写到屏幕上进行显示,也就是说/dev/stdout这个文件就代表了屏幕显示器。那么write(1, buf, strlen(buf))实现的功能就是将buf缓存中的数据写到屏幕进行显示,其数据传输过程如下:

这里写图片描述

  前面我们在分析printf调用过程就说过,printf底层调用的是write函数,而write函数会往1这个文件描述符写数据,而文件描述符1对于的就是stdout标准输出文件,也就是把数据写到显示器,我们可以通过下面这个示例程序1来说明这个过程。

示例1

#include <unistd.h>
int main(void)
{
    int value = 65;
    //通过write输出整数65,看会输出什么
    write(1 , &value , 4);
    //换行
    write(1 , “\n” , 1);
    return 0;
}

执行结果:

这里写图片描述

   程序执行之后,打印的是字符A,而不是65。
   原因在于:凡是要输出到屏幕显示的都必须转成字符,当我们在输出65时,应用程序会把65当做字符A的ASCII编码转换,转换后就会在屏幕显示字符A,所以当程序执行时,我们在屏幕上看到显示的是字符A,而不是字符65。

   如示例2所示,如果要在屏幕上显示字符65,必须将整型65转换成字符‘6’和‘5’进行输出才行,也就是输出6和5这两个字符的ASCII码值。

示例2

#include <unistd.h>
int main(void)
{
    int value = 65;
    char buf1[3] = {0};
    //把整数65转换成字符6和5的ASCII码值进行输出
    buf1[0] = value / 10 + '0';
    buf1[1] = value % 10 + '0';
    buf1[2] = '\n';
    write(1, buf1, 3);
    //printf函数可以指定输出格式
    //printf("%d\n" , value);   
    return 0;
}

执行结果:

这里写图片描述

  而printf函数在输出时通过%d,%s等可以自动转换为对应的字符格式,然后write输出,不需要自己手动转换格式。

5. stderr标准出错文件

  /dev/stderr标准出错输出文件,进程默认通过open(“/dev/stderr”, O_WRONLY)将其打开,返回的文件描述符是2,因为前面已经打开了标志输入文件和标准输出文件。把文件描述符2当成参数传递给write函数,可以将报错信息写(打印)到屏幕上显示。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/80699521