天天看点

scanf 缓冲区原理(你想知道的C语言 2.5)

在正式解说scanf缓冲区之前,我们先吃个开胃小菜.

Q: 为什么调用scanf函数线程会停住等待输入?

A: scanf会调用read系统调用获取用户输入的信息, 在没有准备好输入数据时, read系统调用会进入等待状态.

scanf
    __svfscanf_l
        __srefill
            __srefill1
                _sread
                    _read(read系统调用)
           

  __svfscanf_l:

scanf 缓冲区原理(你想知道的C语言 2.5)
scanf 缓冲区原理(你想知道的C语言 2.5)

    __srefill1会初始化输入缓冲区buffer的数据.

    scanf输入缓冲区和printf输出缓冲区都在FILE结构体, scanf主要关注在struct __sbuf _bf(整个缓冲区)和unsigned char *_p(当前缓冲位置).

    参考: printf 缓冲区原理 (你想知道的C语言 1.4)

Q: 如何dump输入缓冲区数据?

A: 一样可以从FILE结构体_p成员获取缓冲数据.

scanf 缓冲区原理(你想知道的C语言 2.5)
/*
   Xi Chen([email protected])
   cxsjabcabc
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

void check_stdin_buffer()
{
	FILE *fp = stdin;
	unsigned char *p;
	int size;

	p = stdin->_p;
	size = p ? strlen((const char *)p) : 0;

	write(1, "stdin buffer:", strlen("stdin buffer:"));
	write(1, p, size);
	write(1, "\n", 1);
}

int main(int argc, char *argv[])
{
	int n;

	// stdin buffer should be empty now!
	check_stdin_buffer();

	// input one integer
	scanf("%d", &n);
	// current input buffer should be the extra string getting rid of the previous int
	check_stdin_buffer();

	// set the current input buffer character to 'X'
	stdin->_p[0] = 'X';	
	check_stdin_buffer();

	return 0;
}
           

  https://github.com/cxsjabc/basic/blob/dev/c/_topics/scanf/stdin_buffer.c

  运行结果:  

scanf 缓冲区原理(你想知道的C语言 2.5)

        Note: 输入123ZY, scanf %d吃掉123 3个字符, 缓冲区剩下ZY, 强制设置缓冲区第一个字符为X, 当前缓冲区为XY.

Q: scanf默认是行缓冲, 可以输入1个字符就返回吗?

A: 这需要设定TTY模式, 专业名词叫termios, 正因为对于终端输入输出, 不同用户可能有不同要求:

  •   需要不等待即返回;
  •   缓冲区1个字符就返回;
  •   输入的字符不需要回显;
  •   '\r'需要被当做'\n';
  •   ....

  termios才格外重要!

/*
   Xi Chen([email protected])
   cxsjabcabc
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

int main(int argc, char *argv[])
{
	int fd;
	struct termios t;
	char buf[5] = {0};

	/* set tty buffer as: max one character */
	tcgetattr(0, &t);
	t.c_lflag &= ~(ICANON);
	t.c_cc[VMIN] = 1;
	tcsetattr(0, TCSANOW, &t);
	tcgetattr(0, &t);
	printf("VMIN:%d, VTIME:%d\n", t.c_cc[VMIN], t.c_cc[VTIME]);

	// if input one char, read will return!
	read(0, buf, 4);

	printf("buffer: %s\n", buf);

	return 0;
}
           

  一旦输入任何有效字符, read就会立即返回, 不会等到行结束符.

Q: 更进一步, 当输入缓冲区没数据, 可以根本不等待就返回吗?

A: 可以直接修改上面程序t.c_cc[VMIN] = 1 改为 0.

     另外,也可以利用NON_BLOCK标志位:

/*
   Xi Chen([email protected])
   cxsjabcabc
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
	int ret;
	char buf[16] = {0};
	int fd;

	// stdin as non block
	fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
	if (fd < 0) {
		printf("open tty error!\n");
		return -1;
	}

	// if no input data, return immediately!
	ret = read(fd, buf, 15);
	if (ret > 0)
		printf("|%s|\n", buf);
	else
		printf("no avaiable data!\n");

	close(fd);
	return 0;
}
           

  程序运行发现没输入缓冲数据立即就返回.

  TTY read系统调用之内核实作

    内核实作非常有趣, 它清晰地展示了如何处理输入数据, 如何回显, 何时从read返回到用户态scanf继续执行, 下回再做分解.

作者:     陈曦
环境:     MacOS 10.14.5
         Apple LLVM version 10.0.1 (clang-1001.0.46.4)
         Target: x86_64-apple-darwin18.6.0
 
         Linux 3.16.83 (Ubuntu)

转载请注明出处