转自:http://blog.csdn.net/sj13051180/article/details/47865803
问题背景
如果要在Java中调用shell脚本时,可以使用Runtime.exec或ProcessBuilder.start。它们都会返回一个Process对象,通过这个Process可以对获取脚本执行的输出,然后在Java中进行相应处理。例如,下面的代码:
- try
- {
- Process process = Runtime.getRuntime().exec(cmd);
-
- process.waitFor();
-
-
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
通常,安全编码规范中都会指出:使用Process.waitfor的时候,可能导致进程阻塞,甚至死锁。 那么这句应该怎么理解呢?用个实际的例子说明下。
问题描述
使用Java代码调用shell脚本,执行后会发现Java进程和Shell进程都会挂起,无法结束。
Java代码 processtest.java
- try
- {
- Process process = Runtime.getRuntime().exec(cmd);
- System.out.println("start run cmd=" + cmd);
-
- process.waitFor();
- System.out.println("finish run cmd=" + cmd);
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
被调用的Shell脚本doecho.sh
- #!/bin/bash
-
- for((i=0; ;i++))
- do
- echo -n "0123456789"
- echo $i >> count.log
- done
挂起原因
- 主进程中调用Runtime.exec会创建一个子进程,用于执行shell脚本。子进程创建后会和主进程分别独立运行。
- 因为主进程需要等待脚本执行完成,然后对脚本返回值或输出进行处理,所以这里主进程调用Process.waitfor等待子进程完成。
- 通过shell脚本可以看出:子进程执行过程就是不断的打印信息。主进程中可以通过Process.getInputStream和Process.getErrorStream获取并处理。
- 这时候子进程不断向主进程发生数据,而主进程调用Process.waitfor后已挂起。当前子进程和主进程之间的缓冲区塞满后,子进程不能继续写数据,然后也会挂起。
- 这样子进程等待主进程读取数据,主进程等待子进程结束,两个进程相互等待,最终导致死锁。
解决方法
基于上述分析,只要主进程在waitfor之前,能不断处理缓冲区中的数据就可以。因为,我们可以再waitfor之前,单独启两个额外的线程,分别用于处理InputStream和ErrorStream就可以。实例代码如下:
- try
- {
- final Process process = Runtime.getRuntime().exec(cmd);
- System.out.println("start run cmd=" + cmd);
-
-
- new Thread()
- {
- @Override
- public void run()
- {
- BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
- String line = null;
-
- try
- {
- while((line = in.readLine()) != null)
- {
- System.out.println("output: " + line);
- }
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- finally
- {
- try
- {
- in.close();
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- }
- }
- }.start();
-
- new Thread()
- {
- @Override
- public void run()
- {
- BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
- String line = null;
-
- try
- {
- while((line = err.readLine()) != null)
- {
- System.out.println("err: " + line);
- }
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- finally
- {
- try
- {
- err.close();
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- }
- }
- }.start();
-
- process.waitFor();
- System.out.println("finish run cmd=" + cmd);
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
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的子进程。
![](https://img-blog.csdn.net/20150824005613130?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
然后,可以看到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文件的引用。
![](https://img-blog.csdn.net/20150824005750646?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
综上所述,这个过程应该是这样的:子进程不断向pipe中写数据,而父进程一直不读取pipe中的数据,导致pipe被塞满,子进程无法继续写入,所以出现pipe_wait的状态。那么pipe到底有多大呢?
测试pipe的大小
因为我已经在doecho.sh的脚步中记录了打印了字符数,查看count.log就可以知道子进程最终发送了多少数据。在子进程挂起了,count.log的数据一致保持在6543不变。故,当前子进程向pipe中写入6543*10=65430bytes时,出现进程挂起。65536-65430=106byte即距离64K差了106bytes。
![](https://img-blog.csdn.net/20150824013431264?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
换另外的测试方式,每次写入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
- 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
- 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;
- 291 }
- 292 if (bufs)
- 293 continue;
- 294 if (!pipe->writers)
- 295 break;
- 296 if (!pipe->waiting_writers) {
- 297
-
-
-
-
- 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
- 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。
![](https://img-blog.csdn.net/20150824013717117?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
参考资料
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