Use SIGCHLD to reap zombies.

git-svn-id: svn://svn.lyx.org/lyx/lyx-devel/trunk@8526 a592a061-630c-0410-9148-cb99ea01b6c8
This commit is contained in:
Angus Leeming 2004-03-24 17:38:54 +00:00
parent 120e59bc97
commit 4e9ea80fb5
5 changed files with 286 additions and 106 deletions

View File

@ -60,6 +60,7 @@
#include "graphics/Previews.h"
#include "support/filetools.h"
#include "support/forkedcontr.h"
#include "support/globbing.h"
#include "support/path_defines.h"
#include "support/tostr.h"
@ -72,6 +73,7 @@ using lyx::support::AddPath;
using lyx::support::bformat;
using lyx::support::FileFilterList;
using lyx::support::FileSearch;
using lyx::support::ForkedcallsController;
using lyx::support::IsDirWriteable;
using lyx::support::MakeDisplayPath;
using lyx::support::strToUnsignedInt;
@ -520,7 +522,7 @@ void BufferView::Pimpl::selectionRequested()
sel = cur.selectionAsString(false);
if (!sel.empty())
workarea().putClipboard(sel);
}
}
}
@ -584,8 +586,17 @@ void BufferView::Pimpl::update()
// Callback for cursor timer
void BufferView::Pimpl::cursorToggle()
{
if (buffer_)
if (buffer_) {
screen().toggleCursor(*bv_);
// Use this opportunity to deal with any child processes that
// have finished but are waiting to communicate this fact
// to the rest of LyX.
ForkedcallsController & fcc = ForkedcallsController::get();
if (fcc.processesCompleted())
fcc.handleCompletedProcesses();
}
cursor_timeout.restart();
}
@ -862,7 +873,7 @@ bool BufferView::Pimpl::workAreaDispatch(FuncRequest const & cmd0)
return true;
}
#else
case LFUN_MOUSE_MOTION:
case LFUN_MOUSE_MOTION:
#endif
case LFUN_MOUSE_PRESS:

View File

@ -1,3 +1,9 @@
2004-03-24 Angus Leeming <leeming@lyx.org>
* BufferView_pimpl.C (cursorToggle): use the cursor toggle to
deal with any child processes that have finished but are waiting to
communicate this fact to the rest of LyX.
2004-03-24 Angus Leeming <leeming@lyx.org>
64-bit compile fixes.
@ -23,7 +29,7 @@
* lyxfunc.C (getStatus): handle read-only buffers correctly;
handle LFUN_FILE_INSERT_*
* lyxrc.C (setDefaults, getDescription, output, read):
* lyxrc.C (setDefaults, getDescription, output, read):
* lyxrc.h: remove ps_command
2004-03-22 Angus Leeming <leeming@lyx.org>

View File

@ -1,3 +1,14 @@
2004-03-24 Angus Leeming <leeming@lyx.org>
* forkedcontr.[Ch]: get rid of the timer that we use to poll the list
of child proccesses and ascertain whether any have died. Instead use
the SIGCHLD signal emitted by the system to reap these zombies in the
maximally efficient manner. The subsequent emitting of the signal
associated with each child process *is* performed within the main
lyx event loop, thus ensuring that the code remains safe.
A detailed description of the design is to be found in forkedcontr.C.
2004-03-24 Jean-Marc Lasgouttes <lasgouttes@lyx.org>
* filetools.C (i18nLibFileSearch): simplify the logic a bit

View File

@ -17,11 +17,9 @@
#include "forkedcontr.h"
#include "forkedcall.h"
#include "lyxfunctional.h"
#include "debug.h"
#include "frontends/Timeout.h"
#include <boost/bind.hpp>
#include <boost/iterator/indirect_iterator.hpp>
#include <cerrno>
@ -29,14 +27,14 @@
#include <unistd.h>
#include <sys/wait.h>
using boost::bind;
using std::endl;
using std::find_if;
using std::string;
using std::vector;
#ifndef CXX_GLOBAL_CSTD
using std::signal;
using std::strerror;
#endif
@ -44,6 +42,136 @@ using std::strerror;
namespace lyx {
namespace support {
/* The forkedcall controller code handles finished child processes in a
two-stage process.
1. It uses the SIGCHLD signal emitted by the system when the child process
finishes to reap the resulting zombie. The handler routine also
updates an internal list of completed children.
2. The signals associated with these completed children are then emitted
as part of the main LyX event loop.
The guiding philosophy is that zombies are a global resource that should
be reaped as soon as possible whereas an internal list of dead children
is not. Indeed, to emit the signals within the asynchronous handler
routine would result in unsafe code.
The signal handler is guaranteed to be safe even though it may not be
atomic:
int completed_child_status;
sig_atomic_t completed_child_pid;
extern "C"
void child_handler(int)
{
// Clean up the child process.
completed_child_pid = wait(&completed_child_status);
}
(See the signals tutorial at http://tinyurl.com/3h82w.)
It's safe because:
1. wait(2) is guaranteed to be async-safe.
2. child_handler handles only SIGCHLD signals so all subsequent
SIGCHLD signals are blocked from entering the handler until the
existing signal is processed.
This handler performs 'half' of the necessary clean up after a
completed child process. It prevents us leaving a stream of zombies
behind but does not go on to tell the main LyX program to finish the
clean-up by emitting the stored signal. That would most definitely
not be safe.
The only problem with the above is that the global stores
completed_child_status, completed_child_pid may be overwritten before
the clean-up is completed in the main loop.
However, the code in child_handler can be extended to fill an array of
completed processes. Everything remains safe so long as no 'unsafe'
functions are called. (See the list of async-safe functions at
http://tinyurl.com/3h82w.)
struct child_data {
pid_t pid;
int status;
};
// This variable may need to be resized in the main program
// as and when a new process is forked. This resizing must be
// protected with sigprocmask
std::vector<child_data> reaped_children;
sig_atomic_t current_child = -1;
extern "C"
void child_handler(int)
{
child_data & store = reaped_children[++current_child];
// Clean up the child process.
store.pid = wait(&store.status);
}
That is, we build up a list of completed children in anticipation of
the main loop then looping over this list and invoking any associated
callbacks etc. The nice thing is that the main loop needs only to
check the value of 'current_child':
if (current_child != -1)
handleCompletedProcesses();
handleCompletedProcesses now loops over only those child processes
that have completed (ie, those stored in reaped_children). It blocks
any subsequent SIGCHLD signal whilst it does so:
// Used to block SIGCHLD signals.
sigset_t newMask, oldMask;
ForkedcallsController::ForkedcallsController()
{
reaped_children.resize(50);
signal(SIGCHLD, child_handler);
sigemptyset(&oldMask);
sigemptyset(&newMask);
sigaddset(&newMask, SIGCHLD);
}
void ForkedcallsController::handleCompletedProcesses()
{
if (current_child == -1)
return;
// Block the SIGCHLD signal.
sigprocmask(SIG_BLOCK, &newMask, &oldMask);
for (int i = 0; i != 1+current_child; ++i) {
child_data & store = reaped_children[i];
// Go on to handle the child process
...
}
// Unblock the SIGCHLD signal and restore the old mask.
sigprocmask(SIG_SETMASK, &oldMask, 0);
}
Voilà! An efficient, elegant and *safe* mechanism to handle child processes.
*/
namespace {
extern "C"
void child_handler(int)
{
ForkedcallsController & fcc = ForkedcallsController::get();
ForkedcallsController::Data & store =
fcc.reaped_children[++fcc.current_child];
// Clean up the child process.
store.pid = wait(&store.status);
}
} // namespace anon
// Ensure, that only one controller exists inside process
ForkedcallsController & ForkedcallsController::get()
{
@ -53,11 +181,13 @@ ForkedcallsController & ForkedcallsController::get()
ForkedcallsController::ForkedcallsController()
: reaped_children(50), current_child(-1)
{
timeout_ = new Timeout(100, Timeout::ONETIME);
signal(SIGCHLD, child_handler);
timeout_->timeout
.connect(bind(&ForkedcallsController::timer, this));
sigemptyset(&oldMask);
sigemptyset(&newMask);
sigaddset(&newMask, SIGCHLD);
}
@ -66,91 +196,27 @@ ForkedcallsController::ForkedcallsController()
// I want to print or something.
ForkedcallsController::~ForkedcallsController()
{
delete timeout_;
signal(SIGCHLD, SIG_DFL);
}
void ForkedcallsController::addCall(ForkedProcess const & newcall)
{
if (!timeout_->running())
timeout_->start();
forkedCalls.push_back(newcall.clone());
}
if (forkedCalls.size() > reaped_children.size()) {
// Block the SIGCHLD signal.
sigprocmask(SIG_BLOCK, &newMask, &oldMask);
// Timer-call
// Check the list and, if there is a stopped child, emit the signal.
void ForkedcallsController::timer()
{
ListType::iterator it = forkedCalls.begin();
ListType::iterator end = forkedCalls.end();
while (it != end) {
ForkedProcess * actCall = it->get();
reaped_children.resize(2*reaped_children.size());
pid_t pid = actCall->pid();
int stat_loc;
pid_t const waitrpid = waitpid(pid, &stat_loc, WNOHANG);
bool remove_it = false;
if (waitrpid == -1) {
lyxerr << "LyX: Error waiting for child: "
<< strerror(errno) << endl;
// Child died, so pretend it returned 1
actCall->setRetValue(1);
remove_it = true;
} else if (waitrpid == 0) {
// Still running. Move on to the next child.
} else if (WIFEXITED(stat_loc)) {
// Ok, the return value goes into retval.
actCall->setRetValue(WEXITSTATUS(stat_loc));
remove_it = true;
} else if (WIFSIGNALED(stat_loc)) {
// Child died, so pretend it returned 1
actCall->setRetValue(1);
remove_it = true;
} else if (WIFSTOPPED(stat_loc)) {
lyxerr << "LyX: Child (pid: " << pid
<< ") stopped on signal "
<< WSTOPSIG(stat_loc)
<< ". Waiting for child to finish." << endl;
} else {
lyxerr << "LyX: Something rotten happened while "
"waiting for child " << pid << endl;
// Child died, so pretend it returned 1
actCall->setRetValue(1);
remove_it = true;
}
if (remove_it) {
actCall->emitSignal();
forkedCalls.erase(it);
/* start all over: emiting the signal can result
* in changing the list (Ab)
*/
it = forkedCalls.begin();
} else {
++it;
}
}
if (!forkedCalls.empty() && !timeout_->running()) {
timeout_->start();
// Unblock the SIGCHLD signal and restore the old mask.
sigprocmask(SIG_SETMASK, &oldMask, 0);
}
}
// Kill the process prematurely and remove it from the list
// within tolerance secs
void ForkedcallsController::kill(pid_t pid, int tolerance)
ForkedcallsController::iterator ForkedcallsController::find_pid(pid_t pid)
{
typedef boost::indirect_iterator<ListType::iterator> iterator;
@ -159,14 +225,83 @@ void ForkedcallsController::kill(pid_t pid, int tolerance)
iterator it = find_if(begin, end,
lyx::compare_memfun(&Forkedcall::pid, pid));
if (it == end)
return it.base();
}
// Kill the process prematurely and remove it from the list
// within tolerance secs
void ForkedcallsController::kill(pid_t pid, int tolerance)
{
ListType::iterator it = find_pid(pid);
if (it == forkedCalls.end())
return;
it->kill(tolerance);
forkedCalls.erase(it.base());
(*it)->kill(tolerance);
forkedCalls.erase(it);
}
if (forkedCalls.empty())
timeout_->stop();
// Check the list of dead children and emit any associated signals.
void ForkedcallsController::handleCompletedProcesses()
{
if (current_child == -1)
return;
// Block the SIGCHLD signal.
sigprocmask(SIG_BLOCK, &newMask, &oldMask);
for (int i = 0; i != 1+current_child; ++i) {
Data & store = reaped_children[i];
if (store.pid == -1) {
lyxerr << "LyX: Error waiting for child: "
<< strerror(errno) << endl;
continue;
}
ListType::iterator it = find_pid(store.pid);
BOOST_ASSERT(it != forkedCalls.end());
ForkedProcess & child = *it->get();
bool remove_it = false;
if (WIFEXITED(store.status)) {
// Ok, the return value goes into retval.
child.setRetValue(WEXITSTATUS(store.status));
remove_it = true;
} else if (WIFSIGNALED(store.status)) {
// Child died, so pretend it returned 1
child.setRetValue(1);
remove_it = true;
} else if (WIFSTOPPED(store.status)) {
lyxerr << "LyX: Child (pid: " << store.pid
<< ") stopped on signal "
<< WSTOPSIG(store.status)
<< ". Waiting for child to finish." << endl;
} else {
lyxerr << "LyX: Something rotten happened while "
"waiting for child " << store.pid << endl;
// Child died, so pretend it returned 1
child.setRetValue(1);
remove_it = true;
}
if (remove_it) {
child.emitSignal();
forkedCalls.erase(it);
}
}
// Reset the counter
current_child = -1;
// Unblock the SIGCHLD signal and restore the old mask.
sigprocmask(SIG_SETMASK, &oldMask, 0);
}
} // namespace support

View File

@ -17,10 +17,10 @@
#define FORKEDCONTR_H
#include <boost/shared_ptr.hpp>
#include <sys/types.h> // needed for pid_t
#include <csignal>
//#include <sys/types.h> // needed for pid_t
#include <list>
class Timeout;
#include <vector>
namespace lyx {
namespace support {
@ -32,6 +32,15 @@ public:
/// Get hold of the only controller that can exist inside the process.
static ForkedcallsController & get();
/// Are there any completed child processes to be cleaned-up after?
bool processesCompleted() const { return current_child != -1; }
/** Those child processes that are found to have finished are removed
* from the list and their callback function is passed the final
* return state.
*/
void handleCompletedProcesses();
/// Add a new child process to the list of controlled processes.
void addCall(ForkedProcess const &);
@ -41,28 +50,36 @@ public:
*/
void kill(pid_t, int tolerance = 5);
struct Data {
Data() : pid(0), status(0) {}
pid_t pid;
int status;
};
/** These data are used by the SIGCHLD handler to populate a list
* of child processes that have completed and been reaped.
* The associated signals are then emitted within the main LyX
* event loop.
*/
std::vector<Data> reaped_children;
sig_atomic_t current_child;
private:
ForkedcallsController();
ForkedcallsController(ForkedcallsController const &);
~ForkedcallsController();
/** This method is connected to the timer. Every XX ms it is called
* so that we can check on the status of the children. Those that
* are found to have finished are removed from the list and their
* callback function is passed the final return state.
*/
void timer();
/// The child processes
typedef boost::shared_ptr<ForkedProcess> ForkedProcessPtr;
typedef std::list<ForkedProcessPtr> ListType;
///
typedef ListType::iterator iterator;
iterator find_pid(pid_t);
/// The child processes
ListType forkedCalls;
/** The timer. Enables us to check the status of the children
* every XX ms and to invoke a callback on completion.
*/
Timeout * timeout_;
/// Used to block SIGCHLD signals.
sigset_t newMask, oldMask;
};
} // namespace support