调用Process.waitfor导致的进程挂起问题及解决

目录
  • 问题背景
  • 问题描述
  • 挂起原因
  • 解决方法
  • JDK上的说明
  • 背后的故事
  • 测试pipe的大小
  • Linux上pipe分析

问题背景

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

例如,下面的代码:

		try
		{
			Process process = Runtime.getRuntime().exec(cmd);
			process.waitFor();
                        //do something ...
		}
		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);

			//处理InputStream的线程
			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的子进程。

然后,可以看到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)
 {
         size_t total_len = iov_iter_count(to);
         struct file *filp = iocb->ki_filp;
         struct pipe_inode_info *pipe = filp->private_data;
         int do_wakeup;
         ssize_t ret;

         /* Null read succeeds. */
         if (unlikely(total_len == 0))
                 return 0;

         do_wakeup = 0;
         ret = 0;
         __pipe_lock(pipe);
         for (;;) {
                 int bufs = pipe->nrbufs;
                 if (bufs) {
                         int curbuf = pipe->curbuf;
                         struct pipe_buffer *buf = pipe->bufs + curbuf;
                         const struct pipe_buf_operations *ops = buf->ops;
                         size_t chars = buf->len;
                         size_t written;
                         int error;

                         if (chars > total_len)
                                 chars = total_len;

                         error = ops->confirm(pipe, buf);
                         if (error) {
                                 if (!ret)
                                         ret = error;
                                 break;
                         }

                         written = copy_page_to_iter(buf->page, buf->offset, chars, to);
                         if (unlikely(written < chars)) {
                                 if (!ret)
                                         ret = -EFAULT;
                                 break;
                         }
                         ret += chars;
                         buf->offset += chars;
                         buf->len -= chars;

                         /* Was it a packet buffer? Clean up and exit */
                         if (buf->flags & PIPE_BUF_FLAG_PACKET) {
                                 total_len = chars;
                                 buf->len = 0;
                         }

                         if (!buf->len) {
                                 buf->ops = NULL;
                                 ops->release(pipe, buf);
                                 curbuf = (curbuf + 1) & (pipe->buffers - 1);
                                 pipe->curbuf = curbuf;
                                 pipe->nrbufs = --bufs;
                                 do_wakeup = 1;
                         }
                         total_len -= chars;
                         if (!total_len)
                                 break;  /* common path: read succeeded */
                 }
                 if (bufs)       /* More to do? */
                         continue;
                 if (!pipe->writers)
                         break;
                 if (!pipe->waiting_writers) {
                         /* syscall merging: Usually we must not sleep
                          * if O_NONBLOCK is set, or if we got some data.
                          * But if a writer sleeps in kernel space, then
                          * we can wait for that data without violating POSIX.
                          */
                         if (ret)
                                 break;
                         if (filp->f_flags & O_NONBLOCK) {
                                 ret = -EAGAIN;
                                 break;
                         }
                 }
                 if (signal_pending(current)) {
                         if (!ret)
                                 ret = -ERESTARTSYS;
                         break;
                 }
                 if (do_wakeup) {
                         wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM);
                         kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
                 }
                 pipe_wait(pipe);
         }
         __pipe_unlock(pipe);

         /* Signal writers asynchronously that there is more room. */
         if (do_wakeup) {
                 wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM);
                 kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
         }
         if (ret > 0)
                 file_accessed(filp);
         return ret;
 }

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 关于Process的waitFor死锁问题及解决方案

    目录 Process的waitFor死锁问题 1.发现问题 2.原因查找 3.造成死锁原理图 问题代码 解决方案 Java中死锁的简单例子及其避免 我们来看一个死锁的简单例子 那么要怎么预防死锁呢? 下面介绍几个常见方法: Process的waitFor死锁问题 1.发现问题 在实际开发中,进行文件操作时,使用Process对文件进行解压操作,程序执行过程中会出现一直卡在那的问题. 2.原因查找 问题在缓冲区这个地方:可执行程序的标准输出比较多,而运行窗口的标准缓冲区不够大,所以发生阻塞.接着来

  • 基于Process#waitFor()阻塞问题的解决

    目录 Process#waitFor()阻塞问题 Process.waitFor()导致主线程堵塞 Process#waitFor()阻塞问题 有时需要在程序中调用可执行程序或脚本命令: Process process = Runtime.getRuntime().exec(shPath); int exitCode = process .waitFor(); Runtime.getRuntime()返回当前应用程序的Runtime对象,该对象的exec()方法指示Java虚拟机创建一个子进程执

  • java调用process线程阻塞问题的解决

    java调用process线程阻塞问题 项目需求中涉及java调用.bat文件进行图像处理,先直接上简略版程序 public void draw(){ //调用bat脚本进行图像处理 Process process = null; InputStream in = null; try { process = Runtime.getRuntime().exec("startup.bat"); //输出测试 // in = process.getInputStream(); // Stri

  • Java Process.waitFor()方法详解

    目录 Java Process.waitFor()方法 描述 声明 参数 返回值 异常 实例 process.waitFor()没有作用 Java Process.waitFor()方法 Process.waitFor()方法 将导致当前线程等待,直到该对象的进程结束,才返回调用. 描述 java.lang.Process.waitFor()方法将导致当前的线程等待,如果必要的话,直到由该Process对象表示的进程已经终止.此方法将立即返回,如果子进程已经终止.如果子进程尚未终止,则调用线程将

  • 调用Process.waitfor导致的进程挂起问题及解决

    目录 问题背景 问题描述 挂起原因 解决方法 JDK上的说明 背后的故事 测试pipe的大小 Linux上pipe分析 问题背景 如果要在Java中调用shell脚本时,可以使用Runtime.exec或ProcessBuilder.start.它们都会返回一个Process对象,通过这个Process可以对获取脚本执行的输出,然后在Java中进行相应处理. 例如,下面的代码: try { Process process = Runtime.getRuntime().exec(cmd); pro

  • 使用Runtime 调用Process.waitfor导致的阻塞问题

    目录 1. 关于Runtime类的小知识 2. Runtime的几个重要的重载方法 3. Runtime的使用方式 4. 卡死原因 5. 解决方案 6. Runtime最优雅的调用方式 1. 关于Runtime类的小知识 Runtime.getRuntime()可以取得当前JVM的运行时环境,这也是在Java中唯一一个得到运行时环境的方法 Runtime中的exit方法是退出JVM 2. Runtime的几个重要的重载方法 方法名 作用 exec(String command); 在单独的进程中

  • java使用Process调用exe程序及Process.waitFor()死锁问题解决

    目录 前言 文章参考 1. 使用process调用exe程序 2. waitfor 问题描述分析 3. 死锁问题解决 总结 前言 最近在开发android的同时也在开发java ,碰到了需要使用java 程序调用exe的需求,这里我使用的 process 来调用的.该篇文章 读完需要8+分钟,文章类型为 小白入门类型,此处主要记录,方便以后学习补充… 如有不正确的地方还望海涵 及 指出…. 文章参考 process参考 waitfor挂起解析 1. 使用process调用exe程序 Proces

  • java process.waitfor返回1的原因及解决

    目录 java process.waitfor返回1 大致的原因如下 process.waitFor()的返回值含义 以下是Linux中返回值对照表 java process.waitfor返回1 java的process,在执行命令时,出现waitfor返回1错误.根据java文档,是operation not permited.但绝大多数情况下,这是瞎扯. 真正原因是,你执行的命令(dos或linux下的命令),java的process执行时候,出错了.而错误本身是在这个命令的字符串上. 大

  • Node.js中防止错误导致的进程阻塞的方法

    在Node.js中,当某个回调函数发生了错误,整个进程都会崩溃,影响后面的代码执行. Node.js这样处理,是因为在发生未被捕获的错误时,进程的状态就不确定.之后也就无法正常工作了.如果错误始终不处理的话,就回一直抛出意料之外的错误,这样不利于调试. 防止错误导致的进程阻塞的方法主要有如下两种: 一. try-catch try-catch允许进行异常捕获,并让代码继续执行下去: 例如: 当函数抛出错误时,代码就停止执行了: (function() { var a = 0; a(); cons

  • redis scan命令导致redis连接耗尽,线程上锁的解决

    使用redis scan方法无法获取connection,导致线程锁死. 0.关键字 redis springboot redistemplate scan try-with-resource 1.异常现象 应用部署后,功能正常使用,但约数小时左右,部分功能接口异常,接口请求无响应. 2.异常排查 查看堆栈信息,jstask pid.首先找到java进程pid:输出堆栈信息至log文件,jstask 30 > stask.log,看到与redis相关的日志,线程状态为waiting. "p

  • 当master down掉后,pt-heartbeat不断重试会导致内存缓慢增长的原因及解决办法

    最近同事反映,在使用pt-heartbeat监控主从复制延迟的过程中,如果master down掉了,则pt-heartbeat则会连接失败,但会不断重试. 重试本无可厚非,毕竟从使用者的角度来说,希望pt-heartbeat能不断重试,直到重新连接上数据库.但是,他们发现,不断的重试会带来内存的缓慢增长. 重现 环境: pt-heartbeat v2.2.19,MySQL社区版 v5.6.31,Perl v5.10.1,RHEL 6.7,内存500M 为了避免数据库启停对pt-heartbea

随机推荐