一、简介
因为linux命令的强大,因此编程时经常会需要直接引用命令的结果,或者是通过执行一个命令,通过命令返回值来做判断。
干这些事的就是system和popen接口函数,他们属于libc库,不属于系统调用
system:执行一个shell命令,获取命令返回值,不关心命令输出
popen:执行一个shell命令,获取命令的输出,也关心命令返回值
二、原理
system:fork一个子进程,在子进程调用execl("/bin/sh","-c",cmd,(char*)0)执行命令,然后在父进程中waitpid,等待子进程结束,并获取命令的返回值
popen:比system多了一个匿名管道,分为读模式和写模式,区别就在与管道的一端是stdin还是stdout。然后在父进程中,管道的另一端文件描述符生成FILE*文件指针。然后对这个指针操作。
三、问题
命令都有fork这一步,由于fork后,子进程继承父进程所有文件描述符,这里会造成问题。
举个例子:
假设进程中已经打开的socket描述符为fd,执行fork后,子进程中也有了同样的描述符fd。
然后使用popen执行命令,在子进程执行命令的的过程中,父进程意外退出,但有一些机制保障并立即又重新启动了。这时就会产生父进程创建socket失败,地址被占用的问题。(这里的前提条件是:父进程重新启动时,子进程的命令还没执行完,也即还没退出,导致文件描述符fd被子进程占用)
这是fork照成问题的一个例子
四、解决方案
自己重写system,popen,并在fork出的子进程执行命令之前中,主动关闭除0、1、2之外的所有描述符
system的代码:
-
int my_system(const char* pCommand)
-
{
-
pid_t pid;
-
int status;
-
int i = 0;
-
-
if(pCommand == NULL)
-
{
-
return (1);
-
}
-
-
if((pid = fork())<0)
-
{
-
status = -1;
-
}
-
else if(pid == 0)
-
{
-
/* close all descriptors in child sysconf(_SC_OPEN_MAX) */
-
for (i = 3; i < sysconf(_SC_OPEN_MAX); i++)
-
{
-
close(i);
-
}
-
-
execl("/bin/sh", "sh", "-c", pCommand, (char *)0);
-
_exit(127);
-
}
-
else
-
{
-
/*当进程中SIGCHLD做SIG_IGN操作时,获取不到status返回值,XOS_System无法正常工作*/
-
while(waitpid(pid, &status, 0) < 0)
-
{
-
if(errno != EINTR)
-
{
-
status = -1;
-
break;
-
}
-
}
-
}
-
-
return status;
-
}
popen的代码:
-
FILE* my_popen(const char *pCommand, const char *pMode)
-
{
-
int i;
-
int parent_end = 0;
-
int child_end = 0;
-
int child_std_end = 0;
-
int pipe_fds[2] = {0};
-
pid_t child_pid = 0;
-
-
if(NULL == pCommand || NULL == pMode || pid == NULL)
-
{
-
return (NULL);
-
}
-
-
if (pipe(pipe_fds) < 0)
-
{
-
return(NULL);
-
}
-
-
/* only allow "r" or "w" */
-
if (pMode[0] == 'r' && pMode[1] == '\0')
-
{
-
parent_end = pipe_fds[0];
-
child_end = pipe_fds[1];
-
child_std_end = STDOUT_FILENO;
-
}
-
else if (pMode[0] == 'w' && pMode[1] == '\0')
-
{
-
parent_end = pipe_fds[1];
-
child_end = pipe_fds[0];
-
child_std_end = STDIN_FILENO;
-
}
-
else
-
{
-
return(NULL);
-
}
-
-
child_pid = fork();
-
if (child_pid < 0)
-
{
-
close(child_end);
-
close (parent_end);
-
return NULL;
-
}
-
else if (child_pid == 0) /* child */
-
{
-
close (parent_end);
-
if (child_end != child_std_end)
-
{
-
dup2 (child_end, child_std_end);
-
close (child_end);
-
}
-
-
/* close all descriptors in child sysconf(_SC_OPEN_MAX) */
-
for (i = 3; i < sysconf(_SC_OPEN_MAX); i++)
-
{
-
close(i);
-
}
-
-
execl("/bin/sh", "sh", "-c", pCommand, (char *) 0);
-
-
_exit(127);
-
}
-
close(child_end);
-
-
return fdopen(parent_end, pMode);
-
}
-
-
int ExeCmdByPopen(char *pCmd, char *pBuf, unsighed int len)
-
{
-
int count = 2000; /*最多读取2000行*/
-
FILE *fp = NULL;
-
char szRead[1024] = {0};
-
char *pTmp = NULL;
-
unsighed int nLen = 0;
-
int status = 0;
-
pid_t pid;
-
-
-
if(NULL == pCmd || 0 == len)
-
{
-
return XERROR;
-
}
-
-
fp = my_popen(pCmd, "r");//通过一个脚本中转,在脚本中关闭所有的文件描述符,exec fd<&-
-
if(NULL == fp)
-
{
-
perror("XOS_ExeCmdByPopen:popen() failed");
-
return XERROR;
-
}
-
-
sleep(1);
-
-
if(NULL != pBuf)
-
{
-
pBuf[0] = '\0';
-
pTmp = pBuf;
-
while(count > 0 && NULL != fgets(pTmp, len, fp))
-
{
-
nLen = strlen(pTmp);
-
if(0 == nLen)/*缓存区不够*/
-
{
-
break;
-
}
-
pTmp += nLen;
-
len -= nLen;
-
count--;
-
}
-
}
-
else
-
{
-
/*防止脚本在执行的时候可能需要进行写操作,但这边父进程函数退出了,导致管道破坏*/
-
szRead[0] = '\0';
-
while(count > 0 && NULL != fgets(szRead, sizeof(szRead)-1, fp))
-
{
-
count--;
-
}
-
}
-
-
return pclose(fp);
-
}
五、新的问题
使用自己写的popen,发现每次调用都产生一个僵尸进程。
僵尸进程:父进程对子进程的退出没做处理,就照成了僵尸进程。只要在父进程中wait或waitpid均可避免僵尸进程,或通过对SIGCHLD信号设置为忽略,但这样会让system失效。当出现后,可以通过杀死父进程的方式消除。
那么问题来了,为什么会产生僵尸进程呢?
经分析pclose和popen的源代码,发现pclose中会调用waitpid,传入的pid是通过在一个链表上比较pf,找到对应的节点,节点中有在popen中存入的子进程pid。问题找到了!
我们的popen正是少了这一步,没有保存子进程pid,那么调用pclose时,找不到pid,因此无法waitpid子进程。
六、改进
在popen中传出pid,并用waitpid和close(fileno(pf))这两句代替pclose
-
FILE* my_popen(const char *pCommand, const char *pMode, pid_t *pid)
-
{
-
int i;
-
int parent_end = 0;
-
int child_end = 0;
-
int child_std_end = 0;
-
int pipe_fds[2] = {0};
-
pid_t child_pid = 0;
-
-
if(NULL == pCommand || NULL == pMode || pid == NULL)
-
{
-
return (NULL);
-
}
-
-
*pid = 0;
-
-
if (pipe(pipe_fds) < 0)
-
{
-
return(NULL);
-
}
-
-
/* only allow "r" or "w" */
-
if (pMode[0] == 'r' && pMode[1] == '\0')
-
{
-
parent_end = pipe_fds[0];
-
child_end = pipe_fds[1];
-
child_std_end = STDOUT_FILENO;
-
}
-
else if (pMode[0] == 'w' && pMode[1] == '\0')
-
{
-
parent_end = pipe_fds[1];
-
child_end = pipe_fds[0];
-
child_std_end = STDIN_FILENO;
-
}
-
else
-
{
-
return(NULL);
-
}
-
-
child_pid = fork();
-
if (child_pid < 0)
-
{
-
close(child_end);
-
close (parent_end);
-
return NULL;
-
}
-
else if (child_pid == 0) /* child */
-
{
-
close (parent_end);
-
if (child_end != child_std_end)
-
{
-
dup2 (child_end, child_std_end);
-
close (child_end);
-
}
-
-
/* close all descriptors in child sysconf(_SC_OPEN_MAX) */
-
for (i = 3; i < sysconf(_SC_OPEN_MAX); i++)
-
{
-
close(i);
-
}
-
-
execl("/bin/sh", "sh", "-c", pCommand, (char *) 0);
-
-
_exit(127);
-
}
-
close(child_end);
-
*pid = child_pid;
-
-
return fdopen(parent_end, pMode);
-
}
-
-
int ExeCmdByPopen(char *pCmd, char *pBuf, unsighed int len)
{
int count = 2000; /*最多读取2000行*/
FILE *fp = NULL;
char szRead[1024] = {0};
char *pTmp = NULL;
unsighed int nLen = 0;
int status = 0;
pid_t pid;
if(NULL == pCmd || 0 == len)
{
return XERROR;
}
fp = my_popen(pCmd, "r", &pid);//通过一个脚本中转,在脚本中关闭所有的文件描述符,exec fd<&-
if(NULL == fp)
{
perror("XOS_ExeCmdByPopen:popen() failed");
return XERROR;
}
sleep(1);
if(NULL != pBuf)
{
pBuf[0] = '\0';
pTmp = pBuf;
while(count > 0 && NULL != fgets(pTmp, len, fp))
{
nLen = strlen(pTmp);
if(0 == nLen)/*缓存区不够*/
{
break;
}
pTmp += nLen;
len -= nLen;
count--;
}
}
else
{
/*防止脚本在执行的时候可能需要进行写操作,但这边父进程函数退出了,导致管道破坏*/
szRead[0] = '\0';
while(count > 0 && NULL != fgets(szRead, sizeof(szRead)-1, fp))
{
count--;
}
}
/* 下面 close和waitpid组合 相当于pclose.
-- 为了解决XOS_Popen与pclose搭配会产生僵尸进程的问题 */
close(fileno(fp));
while(waitpid(pid, &status, 0) < 0)
{
if(errno != EINTR)
{
status = -1;
break;
}
}
return status;
}
阅读(5607) | 评论(0) | 转发(1) |