最近在项目中需要从父进程读取子进程的输出,从网上查了下,可以直接用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;
}
}
...
}
这个函数只节选了关键部分,把宏全部进行了替换,并添加了注释,
- 首先判断是否有缓冲
- 若有缓冲,且缓冲已满,则执行commit将内容写入文件中
- 将数据写入缓冲
- 判断是否是行缓冲,若是行缓冲,且遇到换行符,则执行commit将内容写入文件中
- 如果不带 缓冲,则直接将数据写入文件
__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 */
}
}
...
}
这个函数中省略了与本文分析无关的部分,大致逻辑如下:
- 首先创建FILE结构体,设置文件描述符,并默认全缓冲(0)
- 调用isatty判断是否是终端设备,若是则设置行缓冲标志
- 创建缓冲区
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。