pipe_wait问题_转

转自:调用Process.waitfor导致的进程挂起

最近遇到pipe_wait问题,父进程调用子进程时,子进程阻塞,cat /proc/$child/wchan输出pipe_wait,进程阻塞在pipe_wait不执行,转载文章对此问题分析很透彻。

问题背景

如果要在Java中调用shell脚本时,可以使用Runtime.exec或ProcessBuilder.start。它们都会返回一个Process对象,通过这个Process可以对获取脚本执行的输出,然后在Java中进行相应处理。例如,下面的代码:

通常,安全编码规范中都会指出:使用Process.waitfor的时候,可能导致进程阻塞,甚至死锁。 那么这句应该怎么理解呢?用个实际的例子说明下。

问题描述

使用Java代码调用shell脚本,执行后会发现Java进程和Shell进程都会挂起,无法结束。

Java代码 processtest.java

[java] view plain copy

  1. try
  2. {
  3. Process process = Runtime.getRuntime().exec(cmd);
  4. System.out.println("start run cmd=" + cmd);
  5. process.waitFor();
  6. System.out.println("finish run cmd=" + cmd);
  7. }
  8. catch (Exception e)
  9. {
  10. e.printStackTrace();
  11. }

被调用的Shell脚本doecho.sh

[plain] view plain copy

  1. #!/bin/bash
  2. for((i=0; ;i++))
  3. do
  4. echo -n "0123456789"
  5. echo $i >> count.log
  6. done

挂起原因

  1. 主进程中调用Runtime.exec会创建一个子进程,用于执行shell脚本。子进程创建后会和主进程分别独立运行。
  2. 因为主进程需要等待脚本执行完成,然后对脚本返回值或输出进行处理,所以这里主进程调用Process.waitfor等待子进程完成。
  3. 通过shell脚本可以看出:子进程执行过程就是不断的打印信息。主进程中可以通过Process.getInputStream和Process.getErrorStream获取并处理。
  4. 这时候子进程不断向主进程发生数据,而主进程调用Process.waitfor后已挂起。当前子进程和主进程之间的缓冲区塞满后,子进程不能继续写数据,然后也会挂起。
  5. 这样子进程等待主进程读取数据,主进程等待子进程结束,两个进程相互等待,最终导致死锁。

解决方法

基于上述分析,只要主进程在waitfor之前,能不断处理缓冲区中的数据就可以。因为,我们可以再waitfor之前,单独启两个额外的线程,分别用于处理InputStream和ErrorStream就可以。实例代码如下:

JDK上的说明

By default, the created subprocess does not have its own terminal or console. All its standard I/O (i.e. stdin, stdout, stderr) operations will be redirected to the parent process, where they can be accessed via the streams obtained using the methods getOutputStream(), getInputStream(), and getErrorStream(). The parent process uses these streams to feed input to and get output from the subprocess. Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, or even deadlock.

从JDK的说明中可以看出两点:

  • 如果系统中标准输入输出流使用的bufffer大小有限,所有读写时可能会出现阻塞或死锁。------这点上面已分析
  • 子进程的标准I/O已经被重定向到了父进程。父进程可以通过对应的接口获取到子进程的I/O。------I/O是如何重定向的?

背后的故事

要回答上面的问题可以从系统的层面尝试分析。

首先通过ps命令可以看到,在Linux上多出了两个进程:一个Java进程、一个shell进程,且shell是java的子进程。

然后,可以看到shell进程的状态显示为pipe_w。我刚开始以为pipe_w表示pipe_write。进一步查看/proc/pid/wchan
发现pipe_w其实表示为pipe_wait。通常/proc/pid/wchan表示一个内存地址或进程正在执行的方法名称。因此,这似乎表明该进程
在操作pipe时发生了等待,从而被挂起。我们知道pipe是IPC的一种,通常用于父子进程之间通信。这样我们可以猜测:可能是父子进程之间通过
pipe通信的时候出现了阻塞。

另外,观察父子进程的fd信息,即/proc/pid/fd。可以看到子进程的0/1/2(即:stdin/stdout/stderr)分别被重定向到了三个pipe文件;父亲进程中对应的也有对着三个pipe文件的引用。

综上所述,这个过程应该是这样的:子进程不断向pipe中写数据,而父进程一直不读取pipe中的数据,导致pipe被塞满,子进程无法继续写入,所以出现pipe_wait的状态。那么pipe到底有多大呢?

测试pipe的大小

因为我已经在doecho.sh的脚步中记录了打印了字符数,查看count.log就可以知道子进程最终发送了多少数据。在子进程挂起
了,count.log的数据一致保持在6543不变。故,当前子进程向pipe中写入6543*10=65430bytes时,出现进程挂起。
65536-65430=106byte即距离64K差了106bytes。

换另外的测试方式,每次写入1k,记录总共可以写入多少。进程代码如test_pipe_size.sh所示。测试结果为64K。两次结果相差了106byte,那个这个pipe到底多大?

Linux上pipe分析

最直接的方式就是看源码。Pipe的实现代码主要在linux/fs/pipe.c中,我们主要看pipe_wait方法。

     pipe_read(struct kiocb *iocb, struct iov_iter *to)
    230 {
    231         size_t total_len = iov_iter_count(to);
    232         struct file *filp = iocb->ki_filp;
    233         struct pipe_inode_info *pipe = filp->private_data;
    234         int do_wakeup;
    235         ssize_t ret;
    236
    237         /* Null read succeeds. */
    238         if (unlikely(total_len == 0))
    239                 return 0;
    240
    241         do_wakeup = 0;
    242         ret = 0;
    243         __pipe_lock(pipe);
    244         for (;;) {
    245                 int bufs = pipe->nrbufs;
    246                 if (bufs) {
    247                         int curbuf = pipe->curbuf;
    248                         struct pipe_buffer *buf = pipe->bufs + curbuf;
    249                         const struct pipe_buf_operations *ops = buf->ops;
    250                         size_t chars = buf->len;
    251                         size_t written;
    252                         int error;
    253
    254                         if (chars > total_len)
    255                                 chars = total_len;
    256
    257                         error = ops->confirm(pipe, buf);
    258                         if (error) {
    259                                 if (!ret)
    260                                         ret = error;
    261                                 break;
    262                         }
    263
    264                         written = copy_page_to_iter(buf->page, buf->offset, chars, to);
    265                         if (unlikely(written < chars)) {
    266                                 if (!ret)
    267                                         ret = -EFAULT;
    268                                 break;
    269                         }
    270                         ret += chars;
    271                         buf->offset += chars;
    272                         buf->len -= chars;
    273
    274                         /* Was it a packet buffer? Clean up and exit */
    275                         if (buf->flags & PIPE_BUF_FLAG_PACKET) {
    276                                 total_len = chars;
    277                                 buf->len = 0;
    278                         }
    279
    280                         if (!buf->len) {
    281                                 buf->ops = NULL;
    282                                 ops->release(pipe, buf);
    283                                 curbuf = (curbuf + 1) & (pipe->buffers - 1);
    284                                 pipe->curbuf = curbuf;
    285                                 pipe->nrbufs = --bufs;
    286                                 do_wakeup = 1;
    287                         }
    288                         total_len -= chars;
    289                         if (!total_len)
    290                                 break;  /* common path: read succeeded */
    291                 }
    292                 if (bufs)       /* More to do? */
    293                         continue;
    294                 if (!pipe->writers)
    295                         break;
    296                 if (!pipe->waiting_writers) {
    297                         /* syscall merging: Usually we must not sleep
    298                          * if O_NONBLOCK is set, or if we got some data.
    299                          * But if a writer sleeps in kernel space, then
    300                          * we can wait for that data without violating POSIX.
    301                          */
    302                         if (ret)
    303                                 break;
    304                         if (filp->f_flags & O_NONBLOCK) {
    305                                 ret = -EAGAIN;
    306                                 break;
    307                         }
    308                 }
    309                 if (signal_pending(current)) {
    310                         if (!ret)
    311                                 ret = -ERESTARTSYS;
    312                         break;
    313                 }
    314                 if (do_wakeup) {
    315                         wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM);
    316                         kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
    317                 }
    318                 pipe_wait(pipe);
    319         }
    320         __pipe_unlock(pipe);
    321
    322         /* Signal writers asynchronously that there is more room. */
    323         if (do_wakeup) {
    324                 wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM);
    325                 kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
    326         }
    327         if (ret > 0)
    328                 file_accessed(filp);
    329         return ret;
    330 }  

可以看到Pipe被组织成环状结构,即一个循环链表。链表中的元素为struct pipe_buffer的结构,每个pipe_buffer对于一个page。链表中共有16个元素,即pipe buffer的总大小为16*page。如果page大小为4K,那么pipe buffer的总大小应该为16*4K=64K。

参考资料

Java 中的进程与线程

https://www.ibm.com/developerworks/cn/java/j-lo-processthread/

When Runtime.exec() won‘t

http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=3

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

http://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html

buffering in standard streams

http://www.pixelbeat.org/programming/stdio_buffering/

Todd.log - a place to keep my thoughts onprogramming

http://www.cnblogs.com/weidagang2046/p/io-redirection.html

linux cross reference

http://lxr.free-electrons.com/source/fs/pipe.c#L103

How big is the pipe buffer

http://unix.stackexchange.com/questions/11946/how-big-is-the-pipe-buffer

时间: 2024-08-28 10:21:56

pipe_wait问题_转的相关文章

彻底解决_OBJC_CLASS_$_某文件名&quot;, referenced from:问题(转)

最近在使用静态库时,总是出现这个问题.下面总结一下我得解决方法: 1. .m文件没有导入   在Build Phases里的Compile Sources 中添加报错的文件 2. .framework文件没有导入静态库编译时往往需要一些库的支持,查看你是否有没有导入的库文件同样是在Build Phases里的Link Binary With Libraries中添加 3. 重复编译,可能你之前复制过两个地方,在这里添加过两次,删除时系统没有默认删除编译引用地址在Build Settings里搜索

python_字符_函数

一.字符集和字符编码 1.定义 计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的英文.汉字等字符是二进制数转换之后的结果.通俗的说,按照何种规则将字符存储在计算机中,如'a'用什么表示,称为"编码":反之,将存储在计算机中的二进制数解析显示出来,称为"解码",如同密码学中的加密和解密.在解码过程中,如果使用了错误的解码规则,则导致'a'解析成'b'或者乱码. 字符(Character):是一个信息单位,在计算机里面,一个中文汉字是一个字符,一个英文字母是

Oracle SQL语言基础及环境准备_超越OCP精通Oracle视频教程培训26

Oracle SQL语言基础及环境准备_超越OCP精通Oracle视频教程培训26 本课程介绍: Oracle视频教程,风哥本套oracle教程培训是<<Oracle数据库SQL语言实战培训教程>>的第1/5套:SQL语言之基础入门及环境准备.主要学习Oracle数据库SQL语言基础介绍.PL/SQL语言介绍.数据库SQL对象与数据类型介绍.SQL语言实战环境准备等. Oracle SQL语言之基础及环境准备,课程内容详细如下: Oracle数据库SQL语言基础介绍Oracle数据

Oracle SQL语言DDL和对象管理_超越OCP精通Oracle视频教程培训27

Oracle SQL语言DDL和对象管理_超越OCP精通Oracle视频教程培训27 本课程介绍: Oracle视频教程,风哥本套oracle教程培训是<<Oracle数据库SQL语言实战培训教程>>的第2/5套:Oracle SQL语言DDL和对象管理.主要学习Oracle数据库模式对象和表中可用的数据类型,表/临时表的创建与使用,索引/约束的创建与管理,视图/同义词/序列的创建和使用,触发器/存储过程/包的创建和使用. Oracle SQL语言DDL和对象管理,课程内容详细如下

Oracle SQL语言DML语句与事务_超越OCP精通Oracle视频教程培训28

Oracle SQL语言DML语句与事务_超越OCP精通Oracle视频教程培训28 本课程介绍: Oracle视频教程,风哥本套oracle教程培训是<<Oracle数据库SQL语言实战培训教程>>的第3/5套:Oracle SQL语言DML语句与事务.主要学习Oracle数据库SQL语句INSERT命令,学习使用UPDATE命令,学习使用DELETE命令,学习创建PL/SQL对象,事务概念与控制,锁的检测和锁争用,了解撤销数据. Oracle SQL语言DML语句与事务管理,课

Oracle SQL语言之查询语句_超越OCP精通Oracle视频教程培训29

Oracle SQL语言之查询语句_超越OCP精通Oracle视频教程培训29 本课程介绍: Oracle视频教程,风哥本套oracle教程培训是<<Oracle数据库SQL语言实战培训教程>>的第4/5套:Oracle SQL语言之查询语句.主要学习Oracle数据库SQL查询限制排序.Oracle SQL联接查询.Oracle SQL子查询等. 视频学习地址: http://edu.51cto.com/course/course_id-8047.html Oracle SQL语

STL_算法(21)_ STL_算法_填充新值

STL_算法_填充新值 fill(b, e, v) fill(b, n, v) generate(b, n, p) generate_n(b, n, p) #include<iostream> #include<algorithm> #include<vector> #include<list> // #include<string> using namespace std; int main() { list<string> sli

[[其他教程]] 2015年最新版iOS基础视频_最适合初学者入门

主讲:孙庆虎类型:iOS 适合对象:初学者入门视频介绍:本视频是iOS学院精心录制的免费精华版iOS语言基础视频,该视频特点在于最大程度保证了知识点的完整性,按知识点进行视频录制,每个视频控制在20分钟左右,不会使学生产生疲劳,授课讲究通俗易懂.幽默风趣.绝对干货.通过该视频的学习,相信你能够轻轻松松地入门C语言,为iOS开发学习打下坚实的基础. 视频概况: 第01天内容 01.尚学堂_孙庆虎C语言_mac os x的基本用法02.尚学堂_孙庆虎C语言_mac os x文件系统及常用终端命令03

C++ Primer笔记8_动态内存_智能指针

1.动态内存 C++中,动态内存管理是通过一对运算符完成的:new和delete.C语言中通过malloc与free函数来实现先动态内存的分配与释放.C++中new与delete的实现其实会调用malloc与free. new分配: 分配变量空间: int *a = new int; // 不初始化 int *b = new int(10); //初始化为10 string *str = new string(10, ); 分配数组空间: int *arr = new int[10];//分配的