天天看點

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)

轉載請注明出處