popen函数解析

popen为glibc中的函数

该函数的帮助手册为:

描述

引用度娘说的:

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。

这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose() 返回的终止状态与 shell 已执行 exit 一样。也就是,popen创建管道,执行shell命令将文件流中的某些数据读出

popen()函数通过创建管道、forking和invoke shell来打开一个进程。因为管道是根据定义是单向的,类型参数可以只指定阅读或写,而不能两者;结果流相应地是只读的或只写的。

command参数是指向包含shell命令行的以空结尾的字符串的指针。此命令使用-c标志传递给/bin/sh;命令行解析由shell脚本完成。

type参数指向一个非空的字符串,该字符串必须包含用于读取的字母“r”或写字母“w”。

popen的返回值是个标准的输入、输出流,必须使用Pclose关闭,不是使用fclose关闭。

请注意,输出的popen()流在默认情况下是完全缓冲的

pclose()函数等待关联的进程终止,并返回由weit4(2)返回的命令的退出状态。

返回值

1,调用fork(),pipe()函数失败,或者无法分配内存时,popen函数返回NULL;

2,pclose函数返回-1,如果wait4返回错误,errno被设置。

3,popen函数中分配内存失败时不会设置errno,fork和pipe函数调用失败时会设置errno。如果type参数无效,errno被设置为EINVAL;

4 如果pclose函数无法获取到子进程状态,errno被设置为ECHILD;

https://blog.csdn.net/weixin_27697385/article/details/112814647

子进程退出状态

而子进程的退出状态,常用以下几个宏进行获取。

WIFEXITED(status)

WIFEXITED(status) 若此值为非0 表明进程正常结束。

若上宏为真,此时可通过WEXITSTATUS(status)获取进程退出状态(exit时参数)

示例:

if(WIFEXITED(status)){

printf("退出值为 %d\n", WEXITSTATUS(status));

}

WIFSIGNALED(status)

WIFSIGNALED(status)为非0 表明进程异常终止。

若上宏为真,此时可通过WTERMSIG(status)获取使得进程退出的信号编号

示例:

if(WIFSIGNALED(status)){

printf("使得进程终止的信号编号: %d\n",WTERMSIG(status));

}

验证

主要确认以下几点:

1, WEXITSTATUS等宏,能否正确取得shell退出状态,该函数返回的应该是exit函数的参数,例如:exit -1?

2, popen之后直接调用pclose是否会等待命令执行结束?

3, 如果没有pclose,会如何?

验证代码

#include <stdio.h>

#include <sys/wait.h>

int main(void)

{

int iRet = 0;

FILE *fp = NULL;

char buff[512] = {'\0'};

fp = popen("./test.sh", "r");

if (NULL == fp)

{

printf("popen failed.\n");

return1;

}

/*

while(fgets(buff, sizeof(buff), fp) != NULL)

{

printf("%s", buff);

}

*/

iRet = pclose(fp);

printf("iRet = %d\n", iRet);

printf("wifexited : %d\n", WIFEXITED(iRet));

printf("wifsignaled : %d\n", WIFSIGNALED(iRet));

printf("wifstopped : %d\n", WIFSTOPPED(iRet));

//if (WIFEXITED(iRet))

printf("exit :%d\n", WEXITSTATUS(iRet));

//if (WIFSIGNALED(iRet))

printf("signal :%d\n", WTERMSIG(iRet));

return0;

}

test.sh脚本

#!/bin/sh

#echo"before..." #注意,echo被注释掉,即,不会输出。

sleep30

#echo"after..."

exit 1

验证 WIFEXITED()等宏,可以正确获取test.sh的执行结果

① test.sh没有执行权限时,WEXITSTATUS()的结果与直接执行test.sh的返回值是一致的。

zsy@ubuntu:~/work/popen$ ./test

sh: 1: ./test.sh: Permission denied

iRet = 32256

wifexited : 1

wifsignaled : 0

wifstopped : 0

exit :126

signal :0

zsy@ubuntu:~/work/popen$ ./test.sh

bash: ./test.sh: Permission denied

zsy@ubuntu:~/work/popen$ echo $?

126

② 给test.sh增加权限后,WEXITSTATUS()获取的正是test.sh中的exit 1的结果。

zsy@ubuntu:~/work/popen$ ./test

iRet = 256

wifexited : 1

wifsignaled : 0

wifstopped : 0

exit :1

signal :0

③ popen执行过程中,将shell子进程kill掉,WTERMSIG()获取的是SIGTERM=15。

zsy@ubuntu:~/work/popen$ ps -ef|grep test

zsy 34593000006:54 pts/100:00:00 ./test

zsy 34603459006:54 pts/100:00:00sh -c ./test.sh

zsy 34613460006:54 pts/100:00:00 /bin/sh ./test.sh

zsy@ubuntu:~/work/popen$ kill3460 # 注意kill的pid

zsy@ubuntu:~/work/popen$ ./test

iRet = 15

wifexited : 0

wifsignaled : 1

wifstopped : 0

exit :0

signal :15

注意:

③的例子中,可以看到popen实际上在fork之后,是执行了“sh –c ./test.sh”命令,然后由shell再启动test.sh。所以test.sh实际上是孙子进程。

如果kill的是孙子进程,结果会如何呢?

zsy@ubuntu:~/work/popen$ ps -ef|grep test

zsy 34843000007:05 pts/100:00:00 ./test

zsy 34853484007:05 pts/100:00:00sh -c ./test.sh

zsy 34863485007:05 pts/100:00:00 /bin/sh ./test.sh

zsy@ubuntu:~/work/popen$ kill3486

zsy@ubuntu:~/work/popen$ ./test

Terminated

iRet = 36608

wifexited : 1

wifsignaled : 0

wifstopped : 0

exit :143

signal :0

也就是说,pclose返回的结果认为子进程shell是正常结束了,终了code为143(143=128+15,实际上就是test.sh收到了SIGTERM的值)。

pclose()调用时,确实会阻塞,等待test.sh中的sleep结束,才会返回

但是,如果把sleep前的echo打开,则pclose()并不会阻塞,而是直接返回。如下:

zsy@ubuntu:~/work/popen$ ./test

iRet = 36096

wifexited : 1

wifsignaled : 0

wifstopped : 0

exit :141

signal :0

原因何在呢?其实答案就在WEXITSTATUS()的结果141中。类似于上面kill 孙子进程时的返回值,141=128+13,说明test.sh(孙子进程)实际上接收到了信号SIGPIPE退出,导致shell子进程立刻返回了。

而test.sh收到SIGPIPE的原因,则是因为pclose()的时候,关闭了popen创建的管道,而test.sh的echo命令,想向管道写数据,就会产生SIGPIPE信号。

※因此,可以考虑两种解决方案。一种就是shell里面不要输出;另一种就是在pclose()前调用fgets,保证shell输出都读取出来后,再关闭。

在ubuntu 14.04x64的虚拟机上测试,即使没有pclose(),似乎也没有特别的问题。

但是,在ARM板上子跑的时候,会出现僵尸进程。

pclose与fclose的关系

fclose函数的定义

libio/iofclose.c文件可以看到:

// fclose被重命名了

# define _IO_new_fclose fclose

// 函数参数类型是写在括号外面的,这种写法在现在已经不常见了,远古遗留的东西

int

_IO_new_fclose (fp)

_IO_FILE *fp;

{

int status;

CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)

/* We desperately try to help programs which are using streams in a

strange way and mix old and new functions. Detect old streams

here. */

if (_IO_vtable_offset (fp) != 0)

return _IO_old_fclose (fp);

#endif

/* First unlink the stream. */

if (fp->_IO_file_flags & _IO_IS_FILEBUF)

_IO_un_link ((struct _IO_FILE_plus *) fp);

// 对文件执行加锁,因此flcose是线程安全的

_IO_acquire_lock (fp);

if (fp->_IO_file_flags & _IO_IS_FILEBUF)

status = _IO_file_close_it (fp);

else

status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;

// 释放文件锁

_IO_release_lock (fp);

// 调用文件虚表中的__finish函数

// 虚表会在后面讲到

_IO_FINISH (fp);

if (fp->_mode > 0)

{

#if _LIBC

/* This stream has a wide orientation. This means we have to free

the conversion functions. */

struct _IO_codecvt *cc = fp->_codecvt;

__libc_lock_lock (__gconv_lock);

__gconv_release_step (cc->__cd_in.__cd.__steps);

__gconv_release_step (cc->__cd_out.__cd.__steps);

__libc_lock_unlock (__gconv_lock);

#endif

}

else

{

// 清空备份空间,文件都关闭了,留着也没必要

if (_IO_have_backup (fp))

_IO_free_backup_area (fp);

}

// 如果不是标准输出、标准出错、标准输入,关闭之后就要释放空间。

// 因为其他文件描述都是通过malloc分配得到内存的

// 而这3个标准描述符是全局变量

if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)

{

fp->_IO_file_flags = 0;

free(fp);

}

return status;

}

/// 补充说明,标准输出,标准输入,标准出错声明在libio.h中

struct _IO_FILE_plus;

extern struct _IO_FILE_plus _IO_2_1_stdin_;

extern struct _IO_FILE_plus _IO_2_1_stdout_;

extern struct _IO_FILE_plus _IO_2_1_stderr_;

// 而实现在libio/stdfiles.c中

// DEF_STDFILE 是一个宏定义

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);

DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);

DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

// 我们所熟知的stdin,stdout,stderr 实际上是上面三个结构体的指针

// 定义在libio/stdio.h中

_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;

_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;

_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

pclose函数的定义

函数pclose被宏定义了,在源文件libio/pclose.c

int__new_pclose (fp) FILE *fp;

{

// 实际上调用的是的fclose的函数

return _IO_new_fclose (fp);

}

总结

  • fclose跟pclose的底层实现是一样的。所以使用fclose关闭popen的fd不会报错;

接下来让我们看看为什么会阻塞。

fclose中的block

从上面的fclose的源码中,没有任何有阻塞调用的痕迹,剩下的就是调用其他函数的时候发生了阻塞。

// 实际上啥也没做,不会造成阻塞

CHECK_FILE(fp, EOF);

// 其实现在libio/genops.c下,只是做文件标志清除,不会造成阻塞

_IO_un_link ((struct _IO_FILE_plus *) fp);

// 会调用虚表中的__close函数,可能造成阻塞

status = _IO_file_close_it (fp);

// 会调用虚表中的__finish函数,可能造成阻塞

_IO_FINISH (fp);

// 释放备份空间,不会造成阻塞

_IO_free_backup_area (fp);

这么一轮下来,基本可以确定就是虚表中的__close 或 __finish 函数造成了阻塞。下面我们再来分析一下文件指针中的虚表。

初识文件指针

C文件指针定义
定义在libio/libio.h

struct _IO_FILE {

struct _IO_FILE {

// 文件标志位,标志只读,只写等

int _flags;/* High-order word is _IO_MAGIC; rest is flags. */

#define _IO_file_flags _flags

// 文件读写相关的指针,这里不多做描述

/* The following pointers correspond to the C++ streambuf protocol. */

/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */

char* _IO_read_ptr;/* Current read pointer */

char* _IO_read_end;/* End of get area. */

char* _IO_read_base;/* Start of putback+get area. */

char* _IO_write_base;/* Start of put area. */

char* _IO_write_ptr;/* Current put pointer. */

char* _IO_write_end;/* End of put area. */

char* _IO_buf_base;/* Start of reserve area. */

char* _IO_buf_end;/* End of reserve area. */

/* The following fields are used to support backing up and undo. */

// 文件备份恢复相关的指针

char *_IO_save_base; /* Pointer to start of non-current get area. */

char *_IO_backup_base; /* Pointer to first valid character of backup area */

char *_IO_save_end; /* Pointer to end of non-current get area. */

// 这个没链表没看出来是干嘛的

struct _IO_marker *_markers;

// 指向下一个打开的文件

struct _IO_FILE *_chain;

// 文件描述符,就是fd

int _fileno;

// 不知道干嘛的标志位

int _flags2;

_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */

/* 1+column number of pbase(); 0 is unknown. */

unsigned short _cur_column;

signed char _vtable_offset;

char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

// 文件锁

_IO_lock_t *_lock;

};

glibc 中就是通过虚表中指向不同函数来实现类似虚函数那样的动作的。

popen与fopen的魔术

popen的实现

popen函数的定义在libio/iopopen.c中

_IO_FILE *

_IO_new_popen (command, mode)

const char *command;

const char *mode;

{

// _IO_proc_file结构体并不定义在该函数内部,只是为了阅读方便

//

struct _IO_proc_file

{

//含有虚表的文件指针

struct _IO_FILE_plus file;

/* Following fields must match those in class procbuf (procbuf.h) */

_IO_pid_t pid;

struct _IO_proc_file *next;

};

// locked_FILE 这个结构体是定义在函数内部的

struct locked_FILE

{

struct _IO_proc_file fpx;

#ifdef _IO_MTSAFE_IO

_IO_lock_t lock;

#endif

} *new_f;

_IO_FILE *fp;

new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

if (new_f == NULL)

return NULL;

#ifdef _IO_MTSAFE_IO

new_f->fpx.file.file._lock = &new_f->lock;

#endif

fp = &new_f->fpx.file.file;

_IO_init (fp, 0);

// 虚表的赋值在这里

// _IO_proc_jumps 是一个全局变量定义在libio/iopopen.c中

_IO_JUMPS (&new_f->fpx.file) = &_IO_proc_jumps;

_IO_new_file_init (&new_f->fpx.file);

#if !_IO_UNIFIED_JUMPTABLES

new_f->fpx.file.vtable = NULL;

#endif

// _IO_new_proc_open 在这里会执行fork操作,创建子进程执行管道任务

if (_IO_new_proc_open (fp, command, mode) != NULL)

return (_IO_FILE *) &new_f->fpx.file;

_IO_un_link (&new_f->fpx.file);

free (new_f);

return NULL;

}

// _IO_proc_jumps 定义在libio/iopopen.c中

static const struct _IO_jump_t _IO_proc_jumps = {

JUMP_INIT_DUMMY,

_IO_new_file_finish, // 先flush流,在执行_IO_new_proc_close函数

_IO_new_file_overflow,

_IO_new_file_underflow,

_IO_default_uflow,

_IO_default_pbackfail,

_IO_new_file_xsputn,

_IO_default_xsgetn,

_IO_new_file_seekoff,

_IO_default_seekpos,

_IO_new_file_setbuf,

_IO_new_file_sync,

_IO_file_doallocate,

_IO_file_read,

_IO_new_file_write,

_IO_file_seek,

_IO_new_proc_close, // 执行wait_pid函数,会导致阻塞

_IO_file_stat,

_IO_default_showmanyc,

_IO_default_imbue

};

int

_IO_new_proc_close (fp)

_IO_FILE *fp;

{

/* This is not name-space clean. FIXME! */

int wstatus;

_IO_proc_file **ptr = &proc_file_chain;

_IO_pid_t wait_pid;

int status = -1;

/* Unlink from proc_file_chain. */

#ifdef _IO_MTSAFE_IO

_IO_cleanup_region_start_noarg (unlock);

_IO_lock_lock (proc_file_chain_lock);

#endif

for ( ; *ptr != NULL; ptr = &(*ptr)->next)

{

if (*ptr == (_IO_proc_file *) fp)

{

*ptr = (*ptr)->next;

status = 0;

break;

}

}

#ifdef _IO_MTSAFE_IO

_IO_lock_unlock (proc_file_chain_lock);

_IO_cleanup_region_end (0);

#endif

if (status < 0 || _IO_close (_IO_fileno(fp)) < 0)

return -1;

/* POSIX.2 Rationale: "Some historical implementations either block

or ignore the signals SIGINT, SIGQUIT, and SIGHUP while waiting

for the child process to terminate. Since this behavior is not

described in POSIX.2, such implementations are not conforming." */

do

{

// 造成阻塞的罪魁祸首

wait_pid = _IO_waitpid (((_IO_proc_file *) fp)->pid, &wstatus, 0);

}

while (wait_pid == -1 && errno == EINTR);

if (wait_pid == -1)

return -1;

return wstatus;

}

  • popen是非阻塞的函数,调用fork创建子进程生成管道,执行任务。在创建文件指针时会重新给虚表赋值

  • 重新赋值的虚表中的__close函数被赋值为_IO_new_proc_close该函数会调用waitpid函数导致阻塞。

fopen的实现

fopen的实现定义在libio/iofopen.c函数中

_IO_FILE *

_IO_new_fopen (filename, mode)

const char *filename;

const char *mode;

{

return __fopen_internal (filename, mode, 1);

}

_IO_FILE *

__fopen_internal (filename, mode, is32)

const char *filename;

const char *mode;

int is32;

{

// 定义结构体

struct locked_FILE

{

// 使用具有虚表的函数指针

struct _IO_FILE_plus fp;

#ifdef _IO_MTSAFE_IO

_IO_lock_t lock;

#endif

struct _IO_wide_data wd;

} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

if (new_f == NULL)

return NULL;

#ifdef _IO_MTSAFE_IO

new_f->fp.file._lock = &new_f->lock;

#endif

#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T

_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);

#else

_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);

#endif

// 给虚表赋值

// _IO_file_jumps 是一个全局变量,定义了文件操作相关的函数

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;

_IO_file_init (&new_f->fp);

#if !_IO_UNIFIED_JUMPTABLES

new_f->fp.vtable = NULL;

#endif

if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)

return __fopen_maybe_mmap (&new_f->fp.file);

_IO_un_link (&new_f->fp);

free (new_f);

return NULL;

}

// _IO_file_jumps 定义在libio/fileops.c中

const struct _IO_jump_t _IO_file_jumps =

{

JUMP_INIT_DUMMY,

_IO_file_finish, // 先flush流,再调用_IO_file_close

_IO_file_overflow,

_IO_file_underflow,

_IO_default_uflow,

_IO_default_pbackfail,

_IO_file_xsputn,

_IO_file_xsgetn,

_IO_new_file_seekoff,

_IO_default_seekpos,

_IO_new_file_setbuf,

_IO_new_file_sync,

_IO_file_doallocate,

_IO_file_read,

_IO_new_file_write,

_IO_file_seek,

_IO_file_close, // 实际上调用的是更底层的close函数,不阻塞

_IO_file_stat,

_IO_default_showmanyc,

_IO_default_imbue

};

fopen的函数中,可以知道fopen实际上是一个非阻塞的函数,在创建文件指针的时候,会将非阻塞的finish函数和close函数填入到虚表中,因此fclose实际上是非阻塞的。

waitpid函数

该函数用来获取子进程状态的变化,状态变化包括:

  • 子进程终止;

  • 子进程被信号中断;

在子进程终止的情况下,该函数会释放与子进程相关的资源,否则子进程将成为僵尸进程。

一旦子进程状态变化,该函数立即返回,否则会一直阻塞等待到子进程状态发生变化。

默认情况下,该函数会阻塞,直到子进程终止,即收到SIGCHLD信号。但是该行为可以被option参数修改。option选项值如下:

WNOHANG

return immediately if no child has exited.

WUNTRACED

also return if a child has stopped (but not traced via ptrace(2)). Status for traced children which have stopped is provided even if this option is not specified.

WCONTINUED

also return if a stopped child has been resumed by delivery of SIGCONT

总结

  • pclose 与 fclose函数的底层实现是一样,所以调用fclose去关闭管道是不会出错的,不管是用哪个函数,都是用之前预先挂接的关闭函数来处理的。

  • popen返回文件指针在close时是阻塞的,但是fopen返回的则不会。

  • popen 与 fclose函数的对创建的文件指针赋予不同的虚表,导致了close时的差异。

  • popen返回的文件指针中,其虚表中的close函数调用waitpid,是引起阻塞的根本原因。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李小白20200202

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值