linux中I/O流的缓冲方式——结合uclibc源码分析


最近在项目中需要从父进程读取子进程的输出,从网上查了下,可以直接用popen打开一个子进程,父进程从返回的FILE*读取输出,popen的实现原理是管道,我自己写了一个简单的实现:

#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>

int main()
{
    struct timeval tv;
    while(1){
        if (gettimeofday(&tv, NULL) < 0){
            printf("get time failed\n");
        }
        else{
            printf("child time: %u.%u\n", tv.tv_sec, tv.tv_usec);
        }

        usleep(1000 * 10);

    }

    return 0;
}

以上是子进程的代码,循环输出当前的时间。

#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>

int main()
{
    int pfd[2];
    struct timeval tv;
    
    if (pipe(pfd) < 0){
        printf("pipe failed\n");
        return -1;
    }

    int pid = fork();
    if (pid < 0){
        printf("fork failed\n");
        return -1;
    }

    if (0 == pid){
        //child
        dup2(pfd[1], STDOUT_FILENO);
        close(pfd[1]);
        close(pfd[0]);

        execl("./popen_child", NULL, NULL);
    }
    else{
        //parent
        close(pfd[1]);
        char buf[1024];

        FILE* fp = fdopen(pfd[0], "r");
        while(fgets(buf, 1024, fp) != NULL){
            gettimeofday(&tv, NULL);
            printf("parent time: %u.%u <-> %s\n", tv.tv_sec, tv.tv_usec, buf);
        }
    }

    return 0;
}

以上是父进程的代码,父进程调用pipe创建一个管道,并fork出子进程,在子进程中将管道的输入端作为标准输出,然后执行上面子进程的代码;同时父进程从管道的输出端读取数据,即子进程的输出,并打印出来。

本以为这么简单就完成了,结果一运行:

parent time: 1582116659.575343 <-> child time: 1582116659.421334

parent time: 1582116659.575345 <-> child time: 1582116659.432357

parent time: 1582116659.575348 <-> child time: 1582116659.443271

parent time: 1582116659.575350 <-> child time: 1582116659.454251

parent time: 1582116659.575353 <-> child time: 1582116659.465186

parent time: 1582116659.575358 <-> child time: 1582116659.476195

parent time: 1582116659.575360 <-> child time: 1582116659.487125

parent time: 1582116659.575362 <-> child time: 1582116659.498117

parent time: 1582116659.575364 <-> child time: 1582116659.509097

parent time: 1582116659.575365 <-> child time: 1582116659.520098

parent time: 1582116659.575368 <-> child time: 1582116659.531040

parent time: 1582116659.575370 <-> child time: 1582116659.541989

parent time: 1582116659.575373 <-> child time: 1582116659.552983

parent time: 1582116659.575376 <-> child time: 1582116659.563957

child time每次增长10ms没错,但parent time几乎不变,而且比child time大,由此可见,父进程是每隔一段时间批量输出一次。

产生这种现象的原因就涉及到linux的输出缓冲方式。

标准I/O库的缓冲主要分为3种:全缓冲、行缓冲和不缓冲。终端默认为行缓冲,文件、管道等为全缓冲。下面结合uclibc源码进行分析。

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内部调用了vfprintf,将内容写入stdout,vfprintf函数代码比较长,下面只节选重要部分

int vfprintf(FILE * __restrict op, register const char * __restrict fmt,
			 va_list ap)
{
	...
	while(*fmt){
		...
		PUTC(ch, op);
		...
	}
}

在while循环中解析格式,最终调用PUTC进行输出,PUTC是个宏,

#define PUTC(C,F)      putc_unlocked_sprintf((C),(__FILE_vsnprintf *)(F)

继续跟踪

static void putc_unlocked_sprintf(int c, __FILE_vsnprintf *f)
{
	if (!__STDIO_STREAM_IS_FAKE_VSNPRINTF_NB(&f->f)) {
		putc_unlocked(c, &f->f);
	} else if (f->bufpos < f->bufend) {
		*f->bufpos++ = c;
	}
}

__STDIO_STREAM_IS_FAKE_VSNPRINTF_NB也是一个宏,判断文件描述符等于-2,暂且不管它,正常文件肯定不是-2,调用putc_unlocked,这也是一个宏,最终调用的函数为__fputc_unlocked。

在分析这个函数之前先看一下FILE这个结构体,就是fopen返回的指针类型,其实际类型如下 :

struct __STDIO_FILE_STRUCT {
	unsigned short __modeflags;
	/* There could be a hole here, but modeflags is used most.*/
	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 */

#endif /* __STDIO_BUFFERS */
...
/* Everything after this is unimplemented... and may be trashed. */
#if __STDIO_BUILTIN_BUF_SIZE > 0
	unsigned char __builtinbuf[__STDIO_BUILTIN_BUF_SIZE];
#endif /* __STDIO_BUILTIN_BUF_SIZE > 0 */
};

这里为了便于分析,只保留了结构体中一些相关的成员,其中__modeflags中保存了像O_RDONLY, O_WRONLY, O_APPEND等这些打开方式以及行缓冲、全缓冲、无缓冲等方式,__filedes为对应的文件描述符,__bufstart,__bufend,__bufpos,__bufread分别指向缓冲区的对应位置(如果有的话),从名字就能看出来,在文件打开的时候会创建一个对应的FILE结构体,同时会malloc一块内存作为缓冲区,并把__bufstart指向这块内存,其他几个不赘述了。

下面看__fputc_unlocked函数:

int __fputc_unlocked(int c, register FILE *stream)
{
	...
		FILE* S = stream;
		if ((S)->__bufend - (S)->__bufstart) { /* 首先判断是否有缓冲 */
			/* 全缓冲或行缓冲. */
			if (!((S)->__bufend - (S)->__bufpos) /* 判断缓冲是否已满 */
				&& __stdio_wcommit((S)) /* Commit failed! */
				) {
				goto BAD;
			}
#ifdef __UCLIBC_MJN3_ONLY__
#warning CONSIDER: Should we fail if the commit fails but we now have room?
#endif

			(*(S)->__bufpos++ = (c));//将数据写入缓冲

			if (((S)->__modeflags & __FLAG_LBF)) { //判断是否是行缓冲
				//若是行缓冲且遇到换行符
				if ((((unsigned char) c) == '\n')
					&& __stdio_wcommit((S))) {
					/* Commit failed! */
					(--(S)->__bufpos); /* 如果写失败则撤销 */
					goto BAD;
				}
			}
		} else {
			/* 如果无缓冲,则直接写文件 */
			unsigned char uc = (unsigned char) c;
			if (! __stdio_WRITE(stream, &uc, 1)) {
				goto BAD;
			}
		}
	...
}

这个函数只节选了关键部分,把宏全部进行了替换,并添加了注释,

  1. 首先判断是否有缓冲
  2. 若有缓冲,且缓冲已满,则执行commit将内容写入文件中
  3. 将数据写入缓冲
  4. 判断是否是行缓冲,若是行缓冲,且遇到换行符,则执行commit将内容写入文件中
  5. 如果不带 缓冲,则直接将数据写入文件

__stdio_wcommit内部调用了__stdio_WRITE,而后者最终执行write系统调用将内容写入文件。

printf分析过程到此结束。

那么每个文件的缓冲方式是在哪里决定的呢?下面看下fopen函数

fopen

FILE *fopen(const char * __restrict filename, const char * __restrict mode)
{
	return _stdio_fopen(((intptr_t) filename), mode, NULL, FILEDES_ARG);
}

fopen调用_stdio_fopen:

FILE attribute_hidden *_stdio_fopen(intptr_t fname_or_mode,
				   register const char * __restrict mode,
				   register FILE * __restrict stream, int filedes)
{
	...
	//判断r,w,a等打开方式并设置标志位
	...
	if (!stream) {		  /* Need to allocate a FILE (not freopen). */
		if ((stream = malloc(sizeof(FILE))) == NULL) {
			return stream;
		}
		stream->__modeflags = __FLAG_FREEFILE;
		...
	}
	if (filedes >= 0) {			/* Handle fdopen trickery. */
		stream->__filedes = filedes;
		...
	}
	if (stream->__filedes != INT_MAX) {
		/* NB: fopencookie uses bogus filedes == INT_MAX,
		 * avoiding isatty() in that case.
		 */
		i = errno; /* preserve errno against isatty call. */
		if (isatty(stream->__filedes))
			stream->__modeflags |= __FLAG_LBF; //设置行缓冲标志
		__set_errno(i);
	}

	if (!stream->__bufstart) {
		if ((stream->__bufstart = malloc(BUFSIZ)) != NULL) {
			stream->__bufend = stream->__bufstart + BUFSIZ;
			stream->__modeflags |= __FLAG_FREEBUF; //buffer需要手动free的标志
		} else {
# if __STDIO_BUILTIN_BUF_SIZE > 0
			stream->__bufstart = stream->__builtinbuf;
			stream->__bufend = stream->__builtinbuf + sizeof(stream->__builtinbuf);
# else  /* __STDIO_BUILTIN_BUF_SIZE > 0 */
			stream->__bufend = stream->__bufstart;
# endif /* __STDIO_BUILTIN_BUF_SIZE > 0 */
		}
	}
	...
}

这个函数中省略了与本文分析无关的部分,大致逻辑如下:

  1. 首先创建FILE结构体,设置文件描述符,并默认全缓冲(0)
  2. 调用isatty判断是否是终端设备,若是则设置行缓冲标志
  3. 创建缓冲区

isatty函数如下:

/* Return 1 if FD is a terminal, 0 if not.  */
int isatty (int fd)
{
  struct termios term;

  return tcgetattr (fd, &term) == 0;
}

fopen分析过程到此结束。

设置缓冲方式

设置行缓冲可以调用setlinebuf函数:

void setlinebuf(FILE * __restrict stream)
{
#ifdef __STDIO_BUFFERS
	setvbuf(stream, NULL, _IOLBF, (size_t) 0);
#endif
}

内部调用了setvbuf:

int setvbuf(register FILE * __restrict stream, register char * __restrict buf,
			int mode, size_t size)
{
#ifdef __STDIO_BUFFERS
	if (((unsigned int) mode) > 2) {
		__set_errno(EINVAL);
		goto ERROR;
	}

	stream->__modeflags &= ~(__MASK_BUFMODE);	/* Clear current mode */
	stream->__modeflags |= mode * __FLAG_LBF;	/*   and set new one. */

	if ((mode == _IONBF) || !size) {
		size = 0;
		buf = NULL;
	} else if (!buf) {
		if ((__STDIO_STREAM_BUFFER_SIZE(stream) == size) /* Same size or */
			|| !(buf = malloc(size)) /* malloc failed, so don't change. */
			) {
			goto DONE;
		}
		alloc_flag = __FLAG_FREEBUF;
	}

	if (stream->__modeflags & __FLAG_FREEBUF) {
		stream->__modeflags &= ~(__FLAG_FREEBUF);
		free(stream->__bufstart);
	}

	stream->__modeflags |= alloc_flag;
	stream->__bufstart = (unsigned char *) buf;
	stream->__bufend = (unsigned char *) buf + size;
	__STDIO_STREAM_INIT_BUFREAD_BUFPOS(stream);
	__STDIO_STREAM_DISABLE_GETC(stream);
	__STDIO_STREAM_DISABLE_PUTC(stream);

 DONE:
	retval = 0;

 ERROR:
	__STDIO_STREAM_VALIDATE(stream);
	__STDIO_AUTO_THREADUNLOCK(stream);

	return retval;

#else  /* __STDIO_BUFFERS  */

	if (mode == _IONBF) {
		return 0;
	}

	if (((unsigned int) mode) > 2) {
		__set_errno(EINVAL);
	}

	return EOF;

#endif
}

以上函数中可以发现setlinebuf传入的buf是NULL, size是0,所以虽然置位了行缓冲的标志,但实际上却设成了无缓冲模式,不知道这是不是uclibc的一个bug。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值