1. 程式人生 > >linux標準輸入輸出

linux標準輸入輸出

超過 理論 -- happy cal ant 參數 結構體類型 ads

一 簡介

sdtin, stdout, stderr分別稱為標準輸入,標準輸出,標準錯誤輸出, 它們的聲明如下:

/* Standard streams. */
extern FILE *stdin; /* Standard input stream. */
extern FILE *stdout; /* Standard output stream. */
extern FILE *stderr; /* Standard error output stream. */

可以看出是libc定義的指針變量,但是C89/C99規定,它應該是一個宏,於是就有了下面這段:

/* C89/C99 say they‘re macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr

很多時候應用程序io操作並沒有指定操作的文件句柄,比如printf,puts,getchar(), scanf()等,這時就采用標準輸入輸出,看看printf()函數的實現:

int printf(const char * __restrict format, ...)
{
va_list arg;
int rv;


va_start(arg, format);
rv = vfprintf(stdout, format, arg);
va_end(arg);


return rv;
}

printf輸出到stdout上,這是很好理解的。

二 原理

  • 初始化過程

sdtin, stdout, stderr是在哪裏初始化的呢,不難找到如下代碼:

FILE *stdin = _stdio_streams;
FILE *stdout = _stdio_streams + 1;
FILE *stderr = _stdio_streams + 2;

也就是說它們的值在編譯期就指定了,不需要運行時去設置,繼續查看_stdio_streams的定義:

static FILE _stdio_streams[] = {
__STDIO_INIT_FILE_STRUCT(_stdio_streams[0], \
__FLAG_LBF|__FLAG_READONLY, \
0, \
_stdio_streams + 1, \
_fixed_buffers, \
BUFSIZ ),
__STDIO_INIT_FILE_STRUCT(_stdio_streams[1], \
__FLAG_LBF|__FLAG_WRITEONLY, \
1, \
_stdio_streams + 2, \
_fixed_buffers + BUFSIZ, \
BUFSIZ ),
__STDIO_INIT_FILE_STRUCT(_stdio_streams[2], \
__FLAG_NBF|__FLAG_WRITEONLY, \
2, \
NULL, \
NULL, \
0 )
};

特別要註意的是其中的0,1,2文件描述符,FILE是一個結構體類型,定義如下:

struct __STDIO_FILE_STRUCT {
unsigned short __modeflags;
/* There could be a hole here, but modeflags is used most.*/
unsigned char __ungot[2];
int __filedes;
#ifdef __STDIO_BUFFERS
unsigned char *__bufstart;/* pointer to buffer */
unsigned char *__bufend;/* pointer to 1 past end of buffer */
unsigned char *__bufpos;
unsigned char *__bufread; /* pointer to 1 past last buffered read char */

#ifdef __STDIO_GETC_MACRO
unsigned char *__bufgetc_u;/* 1 past last readable by getc_unlocked */
#endif /* __STDIO_GETC_MACRO */
#ifdef __STDIO_PUTC_MACRO
unsigned char *__bufputc_u;/* 1 past last writeable by putc_unlocked */
#endif /* __STDIO_PUTC_MACRO */

#endif /* __STDIO_BUFFERS */

......................
#if __STDIO_BUILTIN_BUF_SIZE > 0
unsigned char __builtinbuf[__STDIO_BUILTIN_BUF_SIZE];
#endif /* __STDIO_BUILTIN_BUF_SIZE > 0 */
};

可以看出_stdio_streams的buffer是固定的:

#ifdef __STDIO_BUFFERS
static unsigned char _fixed_buffers[2 * BUFSIZ];
#endif

BUFSIZ默認大小為4096,但是對於後來fopen打開的文件,緩沖區都是malloc分配的。

0,1,2文件描述是在哪裏打開的?一般來說是繼承父進程的,這樣可以方便的實現重定向和管道操作,父進程先保存0,1,2文件描述符,然後dup 0,1,2,啟動子進程,然後父進程還原保存的0,1,2文件描述符,當然libc啟動時也對0,1,2文件描述符進行了檢查:

__check_one_fd (STDIN_FILENO, O_RDONLY | O_NOFOLLOW);
__check_one_fd (STDOUT_FILENO, O_RDWR | O_NOFOLLOW);
__check_one_fd (STDERR_FILENO, O_RDWR | O_NOFOLLOW);

其中__check_one_fd ()定義為:

static void __check_one_fd(int fd, int mode)
{
/* Check if the specified fd is already open */
if (fcntl(fd, F_GETFD) == -1)
{
/* The descriptor is probably not open, so try to use /dev/null */
int nullfd = open(_PATH_DEVNULL, mode);
/* /dev/null is major=1 minor=3. Make absolutely certain
* that is in fact the device that we have opened and not
* some other wierd file... [removed in uclibc] */
if (nullfd!=fd)
{
abort();
}
}
}

當發現0,1,2沒有打開時,打開/dev/null作為0,1,2

另外libc會調用_stdio_init()對_stdio_streams進行運行時初始化,因為其中有些參數無法編譯器指定,比如緩沖類型:

void attribute_hidden _stdio_init(void)
{
#ifdef __STDIO_BUFFERS
int old_errno = errno;
/* stdin and stdout uses line buffering when connected to a tty. */
if (!isatty(0))
_stdio_streams[0].__modeflags ^= __FLAG_LBF;
if (!isatty(1))
_stdio_streams[1].__modeflags ^= __FLAG_LBF;
__set_errno(old_errno);
#endif
#ifndef __UCLIBC__
/* _stdio_term is done automatically when exiting if stdio is used.
* See misc/internals/__uClibc_main.c and and stdlib/atexit.c. */
atexit(_stdio_term);
#endif
}

判斷是否是tty來確定緩沖的類型,isatty判斷的依據是:ioctl (fd, TCGETS, &k_termios),因為每個tty都對應一個termios,用於line disc配置。

  • 緩沖的類型

#define __FLAG_FBF 0x0000U /* must be 0 */
#define __FLAG_LBF 0x0100U
#define __FLAG_NBF 0x0200U /* (__FLAG_LBF << 1) */

分別表示全緩沖(Full Buffer),行緩沖(Line Buffer)和無緩沖(No Buffer), 全緩沖的意思是:只有當緩沖區滿或沒有足夠的空間時,才進行真正的讀寫操作,常見的普通常規文件。行緩沖:讀寫以一行為基本單位,常見的tty設備。無緩沖:不進行緩沖,直接進行讀寫,常見的stderr, 需要錯誤立即輸出可見。

  • 和open()的區別

只是在open()系統調用的基礎上進行了封裝,中間加入了緩沖管理,最終還是通過系統調用實現真正的讀寫操作。這樣的好處是:大部分用戶的讀寫操作是直接操作緩沖的,因為系統調用執行較慢,盡可能減少系統調用的頻率,可以大大提高程序執行的效率。

  • 緩沖的管理

讀寫都是以單個字符為單位的,下面分別分析一下讀寫過程緩沖區的管理。
讀取操作:

if (__STDIO_STREAM_BUFFER_RAVAIL(stream)) {/* Have buffered? */
return __STDIO_STREAM_BUFFER_GET(stream);
}

如果buffer中read available,則直接讀取buffer中的字符返回,否則表明buffer中可讀數據為空:

if (__STDIO_STREAM_BUFFER_SIZE(stream)) { /* Do we have a buffer? */
__STDIO_STREAM_DISABLE_GETC(stream);
if(__STDIO_FILL_READ_BUFFER(stream)) {/* Refill succeeded? */
__STDIO_STREAM_ENABLE_GETC(stream);/* FBF or LBF */
return __STDIO_STREAM_BUFFER_GET(stream);
}
} else {
unsigned char uc;
if (__stdio_READ(stream, &uc, 1)) {
return uc;
}
}

調用__STDIO_FILL_READ_BUFFER() 填充buffer

#define __STDIO_FILL_READ_BUFFER(S) __stdio_rfill((S))

size_t __stdio_rfill(register FILE *__restrict stream)

{

.......

rv = __stdio_READ(stream, stream->__bufstart,
stream->__bufend - stream->__bufstart);
stream->__bufpos = stream->__bufstart;
stream->__bufread = stream->__bufstart + rv;

}

對於全緩沖,盡可能填滿整個buffer,對於行緩沖,則讀取一行數據,至於tty怎麽讀取一行數據,這裏不展開。在用戶不斷的讀取數據後stream->__bufpos不斷往後移動,當等於stream->__bufread時表明緩沖區讀空了,然後再調用這個函數進行填充。

寫入操作:

if (__STDIO_STREAM_BUFFER_SIZE(stream)) { /* Do we have a buffer? */
/* The buffer is full and/or the stream is line buffered. */
if (!__STDIO_STREAM_BUFFER_WAVAIL(stream) /* Buffer full? */
&& __STDIO_COMMIT_WRITE_BUFFER(stream) /* Commit failed! */
) {
goto BAD;
}

__STDIO_STREAM_BUFFER_ADD(stream, ((unsigned char) c));

if (__STDIO_STREAM_IS_LBF(stream)) {
if ((((unsigned char) c) == ‘\n‘)
&& __STDIO_COMMIT_WRITE_BUFFER(stream)) {
/* Commit failed! */
__STDIO_STREAM_BUFFER_UNADD(stream); /* Undo the write! */
goto BAD;
}

在寫入單個字符之前判斷buffer是否還是足夠的空間寫,如果沒有則提交write系統調用,清空buffer。有足夠的空間後,寫入buffer,最後判斷是否是行緩沖,且有行結束標誌,如果是則提交write系統調用,對於全緩沖不用管,盡量推遲寫入操作,到下次沒有足夠空間寫時才提交write系統調用。__STDIO_COMMIT_WRITE_BUFFER的過程如下:

if ((bufsize = __STDIO_STREAM_BUFFER_WUSED(stream)) != 0) {
stream->__bufpos = stream->__bufstart;
__stdio_WRITE(stream, stream->__bufstart, bufsize);
}

stream->__bufpos是當前寫buffer的位置,提交後等於stream->__bufstart,表明清空buffer。

  • 什麽是ungot?

void scanf_buffer(void)

{

int a , b;

while( scanf("%d%d",&a,&b) != EOF )

printf("%d%d\n",a,b);

}

這是一種非常常見的用法,正常情況下沒什麽問題,但是如果用戶誤輸入,比如輸入CSDN 666666\n 會出現什麽情況呢,好奇的可以運行試驗下,結果是會死循環,為何會死循環呢,這就跟scanf()的實現有關了,scanf從緩沖區取出一個字符,%d表明需要的是數字才對,結果一看不對,又把取出的字符塞回去了,scanf函數錯誤返回,結果緩沖區中的內容仍然為CSDN 666666\n,所以下一次進來由於緩沖區有數據,不等用戶輸入就直接錯誤返回了,因此出現了死循環。

這就是ungot機制,當scanf()取出字符發現不對時,將字符退回緩沖區。另外user也可以調用ungetc()函數push back單個字符到緩沖區。

下面是libc中的一段註釋,從中可以看出一二:
/***********************************************************************/
/* Having ungotten characters implies the stream is reading.
* The scheme used here treats the least significant 2 bits of
* the stream‘s modeflags member as follows:
* 0 0 Not currently reading.
* 0 1 Reading, but no ungetc() or scanf() push back chars.
* 1 0 Reading with one ungetc() char (ungot[1] is 1)
* or one scanf() pushed back char (ungot[1] is 0).
* 1 1 Reading with both an ungetc() char and a scanf()
* pushed back char. Note that this must be the result
* of a scanf() push back (in ungot[0]) _followed_ by
* an ungetc() call (in ungot[1]).
*
* Notes:
* scanf() can NOT use ungetc() to push back characters.
* (See section 7.19.6.2 of the C9X rationale -- WG14/N897.)
*/

if (__STDIO_STREAM_CAN_USE_BUFFER_GET(stream)
&& (c != EOF)
&& (stream->__bufpos > stream->__bufstart)
&& (stream->__bufpos[-1] == ((unsigned char)c))
) {
--stream->__bufpos;
__STDIO_STREAM_CLEAR_EOF(stream); /* Must clear end-of-file flag. */
}

else if (c != EOF) {
__STDIO_STREAM_DISABLE_GETC(stream);


/* Flag this as a user ungot, as scanf does the necessary fixup. */
stream->__ungot[1] = 1;
stream->__ungot[(++stream->__modeflags) & 1] = c;


__STDIO_STREAM_CLEAR_EOF(stream); /* Must clear end-of-file flag. */
}

如果push back的字符是剛剛讀取的,則直接stream->__bufpos減一即可,對於大量使用getc()/ungetc(),可以明顯的提高運行效率,但是如果push back的不是最後從緩沖區讀取的,而是用戶調用ungetc() push back一個其他字符,則走下面的流程,__STDIO_STREAM_DISABLE_GETC(stream)設置下次getc()首先從ungot slot中去讀取,ungot slot就是指這裏的stream->__ungot[2], 那麽可以連續push back多少個字符呢,理論上只有一個,因為scanf只需要一個,但是根據這裏的實現代碼來看,可以有很多個:

stream->__modeflags 表示的含義:

高位 <---------------------------------------------------- 32bit ----------------------------------------------3--------2---------1---------0>低位

Error EOF ungot reading

0 0 1: 表示reading,沒有ungot

push back一個字符後,變為:

0 1 0:

stream->__ungot[1] = 1表示stream->__ungot[0]存放的是ungetc() push back的字符

stream->__ungot[1] = 0 表示 stream->__ungot[0]存放的是scanf() push back的字符

接著繼續push back一個字符後,變為:

0 1 1:

stream->__ungot[0]存放的是scanf() push back的字符

stream->__ungot[1]存放的是ungetc() push back的字符

可以看出,連續push back兩個字符是沒什麽問題的,但是如果接著push back一個字符會發生什麽呢?值變成如下:

1 0 0:

stream->__ungot[0]存放的是ungetc() push back的字符,會覆蓋前面push back的字符,並且__FLAG_UNGOT標誌被清掉,這時去調用getc()是讀取不到push back的字符的,getc()函數的部分代碼如下:

if (stream->__modeflags & __FLAG_UNGOT) { /* Use ungots first. */
unsigned char uc = stream->__ungot[(stream->__modeflags--) & 1];
stream->__ungot[1] = 0;
__STDIO_STREAM_VALIDATE(stream);
return uc;
}

__STDIO_STREAM_CLEAR_EOF(stream)最後調用清除掉EOF標誌,所以如果連續push back多次字符,並不會導致緩沖區溢出或死機,只是push back的字符不見了,程序運行邏輯可能出現問題,為了程序更好的移植性,連續ungetc()的次數不要超過1次。

  • 鎖保護

如果應用是單線程的,則可直接使用無鎖版本的接口,busybox是典型的例子:

/* Busybox does not use threads, we can speed up stdio. */
#ifdef HAVE_UNLOCKED_STDIO
# undef getc
# define getc(stream) getc_unlocked(stream)
# undef getchar
# define getchar() getchar_unlocked()
# undef putc
# define putc(c, stream) putc_unlocked(c, stream)
# undef putchar
# define putchar(c) putchar_unlocked(c)
# undef fgetc
# define fgetc(stream) getc_unlocked(stream)
# undef fputc
# define fputc(c, stream) putc_unlocked(c, stream)
#endif
/* Above functions are required by POSIX.1-2008, below ones are extensions */
#ifdef HAVE_UNLOCKED_LINE_OPS
# undef fgets
# define fgets(s, n, stream) fgets_unlocked(s, n, stream)
# undef fputs
# define fputs(s, stream) fputs_unlocked(s, stream)
#endif

  • 讀寫自動轉換

如果stream->__modeflags沒有設置readonly或writeonly標誌,並且libc配置為支持讀寫自動轉換,則讀寫轉換不需要程序員關心,如果libc不支持自動讀寫轉換,則需要註意了

/* C99: Output shall not be directly followed by input without an
intervening call to the fflush function or to a file positioning
function (fseek, fsetpos, or rewind). */

詳細可參考_trans2r.c和_trans2w.c文件實現。

  • narrow & wide reading

主要跟寬字符相關,如何不支持wchar,則默認是narrow reading方式,narrow以單個字符為單位, wide以兩個字符為單位,需要註意的是流一旦設置後,不可進行改變,除非close後重新打開:

if (!(stream->__modeflags & oflag)) {
if (stream->__modeflags & (__FLAG_NARROW|__FLAG_WIDE)) {
__UNDEFINED_OR_NONPORTABLE;
goto DO_EBADF;
}
stream->__modeflags |= oflag;
}

三 註意事項

  • scanf用法

這裏有一篇寫得還不錯的blog,雖然其中有部分表述不正確,但大量的用法實例還是值得借鑒的:http://blog.csdn.net/kobesdu/article/details/39051399 , 其中要特別註意scanf引起的死循環問題,上面在ungot中已經分析過。

  • fflush清空緩沖區

對於輸出,調用fflush立即執行write操作,同時清空緩沖區,但是對於輸入,我見到的libc版本fflush()函數部分代碼如下:

if (__STDIO_STREAM_IS_WRITING(stream)) {
if (!__STDIO_COMMIT_WRITE_BUFFER(stream)) {
__STDIO_STREAM_DISABLE_PUTC(stream);
__STDIO_STREAM_CLEAR_WRITING(stream);
} else {
retval = EOF;
}

__STDIO_STREAM_IS_WRITING()判斷流是否處於寫操作中,否則返回錯誤,所以為了程序具有可移植性,最好是不要使用fflush來清空輸入緩沖區,而應改用其他的方法。

結束語:這部分內容太過繁雜,精力有限,為了節省時間,感覺很多東西都描述得不太清楚,後面有時間再補充整理吧。

http://blog.csdn.net/whuzm08/article/details/73793688

linux標準輸入輸出