/**************************************************************************** * sched/task/task_exithook.c * * Copyright (C) 2011-2013, 2015. 2018 Gregory Nutt. All rights reserved. * Author: Gregory Nutt * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * 3. Neither the name NuttX nor the names of its contributors may be * used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * ****************************************************************************/ /**************************************************************************** * Included Files ****************************************************************************/ #include #include #include #include #include #include #include #include #include "sched/sched.h" #include "group/group.h" #include "signal/signal.h" #include "task/task.h" /**************************************************************************** * Private Functions ****************************************************************************/ /**************************************************************************** * Name: task_atexit * * Description: * Call any registered atexit function(s) * ****************************************************************************/ #if defined(CONFIG_SCHED_ATEXIT) && !defined(CONFIG_SCHED_ONEXIT) static inline void task_atexit(FAR struct tcb_s *tcb) { FAR struct task_group_s *group = tcb->group; /* Make sure that we have not already left the group. Only the final * exiting thread in the task group should trigger the atexit() * callbacks. * * REVISIT: This is a security problem In the PROTECTED and KERNEL builds: * We must not call the registered function in supervisor mode! See also * on_exit() and pthread_cleanup_pop() callbacks. * * REVISIT: In the case of task_delete(), the callback would execute in * the context the caller of task_delete() cancel, not in the context of * the exiting task (or process). */ if (group && group->tg_nmembers == 1) { #if defined(CONFIG_SCHED_ATEXIT_MAX) && CONFIG_SCHED_ATEXIT_MAX > 1 int index; /* Call each atexit function in reverse order of registration atexit() * functions are registered from lower to higher array indices; they * must be called in the reverse order of registration when the task * group exits, i.e., from higher to lower indices. */ for (index = CONFIG_SCHED_ATEXIT_MAX - 1; index >= 0; index--) { if (group->tg_atexitfunc[index]) { atexitfunc_t func; /* Nullify the atexit function to prevent its reuse. */ func = group->tg_atexitfunc[index]; group->tg_atexitfunc[index] = NULL; /* Call the atexit function */ (*func)(); } } #else if (group->tg_atexitfunc) { atexitfunc_t func; /* Nullify the atexit function to prevent its reuse. */ func = group->tg_atexitfunc; group->tg_atexitfunc = NULL; /* Call the atexit function */ (*func)(); } #endif } } #else # define task_atexit(tcb) #endif /**************************************************************************** * Name: task_onexit * * Description: * Call any registered on_exit function(s) * ****************************************************************************/ #ifdef CONFIG_SCHED_ONEXIT static inline void task_onexit(FAR struct tcb_s *tcb, int status) { FAR struct task_group_s *group = tcb->group; /* Make sure that we have not already left the group. Only the final * exiting thread in the task group should trigger the atexit() * callbacks. * * REVISIT: This is a security problem In the PROTECTED and KERNEL builds: * We must not call the registered function in supervisor mode! See also * atexit() and pthread_cleanup_pop() callbacks. * * REVISIT: In the case of task_delete(), the callback would execute in * he context the caller of task_delete() cancel, not in the context of * the exiting task (or process). */ if (group && group->tg_nmembers == 1) { #if defined(CONFIG_SCHED_ONEXIT_MAX) && CONFIG_SCHED_ONEXIT_MAX > 1 int index; /* Call each on_exit function in reverse order of registration. * on_exit() functions are registered from lower to higher array * indices; they must be called in the reverse order of registration * when the task group exits, i.e., from higher to lower indices. */ for (index = CONFIG_SCHED_ONEXIT_MAX - 1; index >= 0; index--) { if (group->tg_onexitfunc[index]) { onexitfunc_t func; /* Nullify the on_exit function to prevent its reuse. */ func = group->tg_onexitfunc[index]; group->tg_onexitfunc[index] = NULL; /* Call the on_exit function */ (*func)(status, group->tg_onexitarg[index]); } } #else if (group->tg_onexitfunc) { onexitfunc_t func; /* Nullify the on_exit function to prevent its reuse. */ func = group->tg_onexitfunc; group->tg_onexitfunc = NULL; /* Call the on_exit function */ (*func)(status, group->tg_onexitarg); } #endif } } #else # define task_onexit(tcb,status) #endif /**************************************************************************** * Name: task_exitstatus * * Description: * Report exit status when main task of a task group exits * ****************************************************************************/ #ifdef CONFIG_SCHED_CHILD_STATUS static inline void task_exitstatus(FAR struct task_group_s *group, int status) { FAR struct child_status_s *child; /* Check if the parent task group has suppressed retention of * child exit status information. */ if ((group->tg_flags & GROUP_FLAG_NOCLDWAIT) == 0) { /* No.. Find the exit status entry for this task in the parent TCB */ child = group_findchild(group, getpid()); if (child) { #ifndef HAVE_GROUP_MEMBERS /* No group members? Save the exit status */ child->ch_status = status; #endif /* Save the exit status.. For the case of HAVE_GROUP_MEMBERS, * the child status will be as exited until the last member * of the task group exits. */ child->ch_status = status; } } } #else # define task_exitstatus(group,status) #endif /* CONFIG_SCHED_CHILD_STATUS */ /**************************************************************************** * Name: task_groupexit * * Description: * Mark that the final thread of a child task group as exited. * ****************************************************************************/ #ifdef CONFIG_SCHED_CHILD_STATUS static inline void task_groupexit(FAR struct task_group_s *group) { FAR struct child_status_s *child; /* Check if the parent task group has suppressed retention of child exit * status information. */ if ((group->tg_flags & GROUP_FLAG_NOCLDWAIT) == 0) { /* No.. Find the exit status entry for this task in the parent TCB */ child = group_findchild(group, getpid()); if (child) { /* Mark that all members of the child task group has exited */ child->ch_flags |= CHILD_FLAG_EXITED; } } } #else # define task_groupexit(group) #endif /* CONFIG_SCHED_CHILD_STATUS */ /**************************************************************************** * Name: task_sigchild * * Description: * Send the SIGCHILD signal to the parent thread * ****************************************************************************/ #if defined(CONFIG_SCHED_HAVE_PARENT) && !defined(CONFIG_DISABLE_SIGNALS) #ifdef HAVE_GROUP_MEMBERS static inline void task_sigchild(gid_t pgid, FAR struct tcb_s *ctcb, int status) { FAR struct task_group_s *chgrp = ctcb->group; FAR struct task_group_s *pgrp; siginfo_t info; DEBUGASSERT(chgrp); /* Get the parent task group. It is possible that all of the members of * the parent task group have exited. This would not be an error. In * this case, the child task group has been orphaned. */ pgrp = group_findbygid(pgid); if (!pgrp) { /* Set the task group ID to an invalid group ID. The dead parent * task group ID could get reused some time in the future. */ chgrp->tg_pgid = INVALID_GROUP_ID; return; } /* Save the exit status now if this is the main thread of the task group * that is exiting. Only the exiting main task of a task group carries * interpretable exit Check if this is the main task that is exiting. */ #ifndef CONFIG_DISABLE_PTHREAD if ((ctcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_PTHREAD) #endif { task_exitstatus(pgrp, status); } /* But only the final exiting thread in a task group, whatever it is, * should generate SIGCHLD. */ if (chgrp->tg_nmembers == 1) { /* Mark that all of the threads in the task group have exited */ task_groupexit(pgrp); /* Create the siginfo structure. We don't actually know the cause. * That is a bug. Let's just say that the child task just exited * for now. */ info.si_signo = SIGCHLD; info.si_code = CLD_EXITED; info.si_errno = OK; info.si_value.sival_ptr = NULL; #ifndef CONFIG_DISABLE_PTHREAD info.si_pid = chgrp->tg_task; #else info.si_pid = ctcb->pid; #endif info.si_status = status; /* Send the signal to one thread in the group */ (void)group_signal(pgrp, &info); } } #else /* HAVE_GROUP_MEMBERS */ static inline void task_sigchild(FAR struct tcb_s *ptcb, FAR struct tcb_s *ctcb, int status) { siginfo_t info; /* If task groups are not supported then we will report SIGCHLD when the * task exits. Unfortunately, there could still be threads in the group * that are still running. */ #ifndef CONFIG_DISABLE_PTHREAD if ((ctcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_PTHREAD) #endif { #ifdef CONFIG_SCHED_CHILD_STATUS /* Save the exit status now of the main thread */ task_exitstatus(ptcb->group, status); #else /* CONFIG_SCHED_CHILD_STATUS */ /* Exit status is not retained. Just decrement the number of * children from this parent. */ DEBUGASSERT(ptcb->group != NULL && ptcb->group->tg_nchildren > 0); ptcb->group->tg_nchildren--; #endif /* CONFIG_SCHED_CHILD_STATUS */ /* Create the siginfo structure. We don't actually know the cause. * That is a bug. Let's just say that the child task just exited * for now. */ info.si_signo = SIGCHLD; info.si_code = CLD_EXITED; info.si_errno = OK; info.si_value.sival_ptr = NULL; #ifndef CONFIG_DISABLE_PTHREAD info.si_pid = ctcb->group->tg_task; #else info.si_pid = ctcb->pid; #endif info.si_status = status; /* Send the signal. We need to use this internal interface so that we * can provide the correct si_code value with the signal. */ (void)nxsig_tcbdispatch(ptcb, &info); } } #endif /* HAVE_GROUP_MEMBERS */ #else /* CONFIG_SCHED_HAVE_PARENT && !CONFIG_DISABLE_SIGNALS */ # define task_sigchild(x,ctcb,status) #endif /* CONFIG_SCHED_HAVE_PARENT && !CONFIG_DISABLE_SIGNALS */ /**************************************************************************** * Name: task_signalparent * * Description: * Send the SIGCHILD signal to the parent task group * ****************************************************************************/ #ifdef CONFIG_SCHED_HAVE_PARENT static inline void task_signalparent(FAR struct tcb_s *ctcb, int status) { #ifdef HAVE_GROUP_MEMBERS DEBUGASSERT(ctcb && ctcb->group); /* Keep things stationary throughout the following */ sched_lock(); /* Send SIGCHLD to all members of the parent's task group */ task_sigchild(ctcb->group->tg_pgid, ctcb, status); sched_unlock(); #else FAR struct tcb_s *ptcb; /* Keep things stationary throughout the following */ sched_lock(); /* Get the TCB of the receiving, parent task. We do this early to * handle multiple calls to task_signalparent. ctcb->group->tg_ppid is * set to an invalid value below and the following call will fail if we * are called again. */ ptcb = sched_gettcb(ctcb->group->tg_ppid); if (ptcb == NULL) { /* The parent no longer exists... bail */ sched_unlock(); return; } /* Send SIGCHLD to all members of the parent's task group */ task_sigchild(ptcb, ctcb, status); /* Forget who our parent was */ ctcb->group->tg_ppid = INVALID_PROCESS_ID; sched_unlock(); #endif } #else # define task_signalparent(ctcb,status) #endif /**************************************************************************** * Name: task_exitwakeup * * Description: * Wakeup any tasks waiting for this task to exit * ****************************************************************************/ #if defined(CONFIG_SCHED_WAITPID) && !defined(CONFIG_SCHED_HAVE_PARENT) static inline void task_exitwakeup(FAR struct tcb_s *tcb, int status) { FAR struct task_group_s *group = tcb->group; /* Have we already left the group? */ if (group) { /* Only tasks (and kernel threads) return valid status. Record the * exit status when the task exists. The group, however, may still * be executing. */ #ifndef CONFIG_DISABLE_PTHREAD if ((tcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_PTHREAD) #endif { /* Report the exit status. We do not nullify tg_statloc here * because we want to prevent other tasks from registering for * the return status. There is only one task per task group, * there for, this logic should execute exactly once in the * lifetime of the task group. * * "If more than one thread is suspended in waitpid() awaiting * termination of the same process, exactly one thread will * return the process status at the time of the target process * termination." * * Hmmm.. what do we return to the others? */ if (group->tg_statloc) { *group->tg_statloc = status << 8; } } /* Is this the last thread in the group? */ if (group->tg_nmembers == 1) { /* Yes.. Wakeup any tasks waiting for this task to exit */ group->tg_statloc = NULL; group->tg_waitflags = 0; while (group->tg_exitsem.semcount < 0) { /* Wake up the thread */ nxsem_post(&group->tg_exitsem); } } } } #else # define task_exitwakeup(tcb, status) #endif /**************************************************************************** * Name: task_flushstreams * * Description: * Flush all streams when the final thread of a group exits. * ****************************************************************************/ #if CONFIG_NFILE_STREAMS > 0 static inline void task_flushstreams(FAR struct tcb_s *tcb) { FAR struct task_group_s *group = tcb->group; /* Have we already left the group? Are we the last thread in the group? */ if (group && group->tg_nmembers == 1) { #if (defined(CONFIG_BUILD_PROTECTED) || defined(CONFIG_BUILD_KERNEL)) && \ defined(CONFIG_MM_KERNEL_HEAP) (void)lib_flushall(tcb->group->tg_streamlist); #else (void)lib_flushall(&tcb->group->tg_streamlist); #endif } } #else # define task_flushstreams(tcb) #endif /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** * Name: task_exithook * * Description: * This function implements some of the internal logic of exit() and * task_delete(). This function performs some clean-up and other actions * required when a task exits: * * - All open streams are flushed and closed. * - All functions registered with atexit() and on_exit() are called, in * the reverse order of their registration. * * When called from exit(), the tcb still resides at the head of the ready- * to-run list. The following logic is safe because we will not be * returning from the exit() call. * * When called from task_terminate() we are operating on a different thread; * on the thread that called task_delete(). In this case, task_delete * will have already removed the tcb from the ready-to-run list to prevent * any further action on this task. * * nonblocking will be set true only when we are called from task_terminate() * via _exit(). In that case, we must be careful to do nothing that can * cause the cause the thread to block. * ****************************************************************************/ void task_exithook(FAR struct tcb_s *tcb, int status, bool nonblocking) { /* Under certain conditions, task_exithook() can be called multiple times. * A bit in the TCB was set the first time this function was called. If * that bit is set, then just exit doing nothing more.. */ if ((tcb->flags & TCB_FLAG_EXIT_PROCESSING) != 0) { return; } #ifdef CONFIG_CANCELLATION_POINTS /* Mark the task as non-cancelable to avoid additional calls to exit() * due to any cancellation point logic that might get kicked off by * actions taken during exit processing. */ tcb->flags |= TCB_FLAG_NONCANCELABLE; tcb->flags &= ~TCB_FLAG_CANCEL_PENDING; tcb->cpcount = 0; #endif #if defined(CONFIG_SCHED_ATEXIT) || defined(CONFIG_SCHED_ONEXIT) /* If exit function(s) were registered, call them now before we do any un- * initialization. * * NOTES: * * 1. In the case of task_delete(), the exit function will *not* be called * on the thread execution of the task being deleted! That is probably * a bug. * 2. We cannot call the exit functions if nonblocking is requested: These * functions might block. * 3. This function will only be called with with non-blocking == true * only when called through _exit(). _exit() behaviors requires that * the exit functions *not* be called. */ if (!nonblocking) { task_atexit(tcb); /* Call any registered on_exit function(s) */ task_onexit(tcb, status); } #endif /* If the task was terminated by another task, it may be in an unknown * state. Make some feeble effort to recover the state. */ task_recover(tcb); /* Send the SIGCHILD signal to the parent task group */ task_signalparent(tcb, status); /* Wakeup any tasks waiting for this task to exit */ task_exitwakeup(tcb, status); /* If this is the last thread in the group, then flush all streams (File * descriptors will be closed when the TCB is deallocated). * * NOTES: * 1. We cannot flush the buffered I/O if nonblocking is requested. * that might cause this logic to block. * 2. This function will only be called with with non-blocking == true * only when called through _exit(). _exit() behavior does not * require that the streams be flushed */ if (!nonblocking) { task_flushstreams(tcb); } #ifdef HAVE_TASK_GROUP /* Leave the task group. Perhaps discarding any un-reaped child * status (no zombies here!) */ group_leave(tcb); #endif #ifndef CONFIG_DISABLE_SIGNALS /* Deallocate anything left in the TCB's queues */ nxsig_cleanup(tcb); /* Deallocate Signal lists */ #endif /* This function can be re-entered in certain cases. Set a flag * bit in the TCB to not that we have already completed this exit * processing. */ tcb->flags |= TCB_FLAG_EXIT_PROCESSING; }