Shell主要逻辑源码级分析 (2)——SHELL作业控制
版权声明:本文由李航原创文章,转载请注明出处:
文章原文链接:https://www.qcloud.com/community/article/110
来源:腾云阁 https://www.qcloud.com/community
作业控制本身也是基于进程控制的,两者关系密切,所以SHELL进程控制与作业控制的机制都在本章描述。
一. 主要相关文件
jobs.c
jobs.h
nojobs.c
备注:其中nojobs.c用于在早期的一些不支持作业控制的操作系统中代替jobs.c编译,其函数接口集是jobs.c的子集,而且现今的主要操作系统都是支持作业控制的,因此并未专门注释nojobs.c文件,而详细注释了jobs.c文件。如果需要查看nojobs.c中部分函数的功能,则直接查找对应的jobs.c的版本即可。
二. 重要数据结构
进程:
typedef struct process {
struct process *next; /* 指向管道中的下一个进程*/
pid_t pid; /* 进程id */
WAIT status; /* wait等待该进程所返回的结果*/
int running; /* 是否处于运行状态,共有PS_DONE、PS_RUNNING、
PS_STOPPED、PS_RECYCLED*/
char *command; /* 该进程所正在执行的命令*/
} PROCESS;
该结构描述了一个shell里定义的进程。其中command成员记录了该进程所execve执行的命令的名称。
作业:
typedef struct job {
char *wd; /* 工作目录*/
PROCESS *pipe; /* 构成当前作业的进程组的管道链表,是个循环链表*/
pid_t pgrp; /*该管道进程组的PID*/
JOB_STATE state; /* 当前作业的状态*/
int flags; /* 作业标记,共有J_NOHUP、J_FOREGROUND、J_NOTIFIED、
J_JOBCONTROL、J_STATSAVED、J_ASYNC6个标记 */
#if defined (JOB_CONTROL)
COMMAND *deferred; /* 本作业完成时要执行的工作*/
sh_vptrfunc_t *j_cleanup; /* 作业被标志为死亡时要调用的清理函数*/
PTR_T cleanarg; /* 要传递给j_cleanup 成员的参数*/
#endif /* JOB_CONTROL */
} JOB;
该结构定义了一个作业结构体。可以看到,一个作业下面可以有数个进程组成,该进程组存储在pipe成员为入口的循环链表中.这些进程也构成了一个作业的主体,即作业要做的各个步骤的工作。而该作业的返回状态可以认为等同于管道中最后一个进程的返回状态。全局作业数组:JOB **jobs
存储当前shell下所有的作业结构的数组。数组下标即该作业的作业号。
全局作业状态:
struct jobstats {
long c_childmax;/*允许的最大子进程数*/
/*以下3个成员为子进程的统计信息*/
int c_living; /*正在运行或暂停(即非死亡)的进程数*/
int c_reaped; /* 死亡但还位于joblist中的进程数 */
int c_injobs; /*jobs list中的子进程总数 */
/* 子进程总数 */
int c_totforked; /* 当前shellfork产生的子进程总数 */
int c_totreaped; /*当前shell已经移除掉的子进程总数*/
/* 以下5个值都是job的下标或者计数器 */
int j_jobslots; /* jobs数组的总容量*/
int j_lastj; /* 最后一个分配的作业的作业号 */
int j_firstj; /*第一个分配的作业的作业号*/
int j_njobs; /* 作业数组中的非空作业的总数 */
int j_ndead; /* 作业数组中所有死亡作业的总数 */
int j_current; /* 当前作业的作业号 */
int j_previous; /* 前一作业的作业号*/
JOB *j_lastmade; /* stop_pipeline函数分配的最后一个作业的作业号 */
JOB *j_lastasync; /* stop_pipeline 函数分配的最后一个异步作业的作业号*/
};
保存当前shell作业管理状态的结构体。记录了全局作业数组jobs的相关信息。
后台进程链表:
struct pidstat {
struct pidstat *next;/*指向下一个进程的指针*/
pid_t pid;/*该进程的PID*/
int status;/*该进程的退出状态*/
};
维护一个后台进程的信息的节点结构。
struct bgpids {
struct pidstat *list;/*链表头节点*/
struct pidstat *end;/*链表尾节点*/
int npid;/*后台运行的进程总数*/
};
struct bgpids bgpids = { 0, 0, 0 };
维护所有后台运行的进程的状态的链表。shell中用bgpids
结构维护所有在后台运行的进程的信息,并提供了bgp_alloc
、bgp_add
、bgp_delete
、bgp_clear
、bgp_search
、bgp_prune
等一组接口函数来对其进行操作。
三. 作业控制机制
附:与作业控制相关的shell命令
命令 | 含义 |
---|---|
bg | 启动被终止的后台作业 |
fg | 将后台作业调到前台来 |
jobs | 列出所有正在运行的作业 |
kill | 向指定作业发送kill信号 |
stop | 挂起一个后台作业 |
stty tostop | 当一个后台作业向终端发送输出时就挂起它 |
wait[n] | 等待一个指定的作业并返回它的退出状态,这里n是一个PID或作业号 |
∧Z(Ctrl-Z) | 终止(挂起)作业。屏幕上将出现提示符 |
jobs命令的参数 | 含义 |
---|---|
%n | 作业号n |
%string | 以string开头的作业名 |
%?string | 作业名包含string |
%% | 当前作业 |
%+ | 当前作业 |
%- | 当前作业前的一个作业 |
-r | 列出所有运行的作业 |
-s | 列出所有挂起的作业 |
在很老的版本的shell中,是不支持作业控制的,只允许同时存在一个作业。作业控制的意思就是:允许在一个终端启动多个作业,是BSD于1980年加入的
目前ksh,bash,csh都支持作业控制。
首先介绍两个概念,前台作业和后台作业。一般运行的作业都是前台作业,即在前台运行,占用终端。前台作业一旦运行起来,除非收到信号被挂起或者终止或者运行结束,都会一直将终端挂起,导致无法启动其他作业。后台作业则是在后台运行,不阻塞终端和键盘的使用。
1.前台作业和后台作业的关系:
本质上来说没特别大的区别,主要在于是否阻塞键盘输入。他们共享键盘、显示器、CPU时间片等资源。因此如果不对后台作业的输出进行重定向,则其在后台运行的输出结果会不时的在终端打印出来。因此习惯上来说,后台作业的输出都会进行重定向。由于前台作业会阻塞键盘输入,因此会长时间执行的作业一般放在后台执行。可以先将前台作业挂起,然后用BG命令将其转入后台执行,也可以用fg命令将后台作业转到前台执行,因此前后台作业之间是可以相互转化的。
2.作业启动流程图:
下图是start_job函数的执行流程图,即shell下启动一个作业的流程。
改变作业状态shell中提供了fg和bg命令来改变作业的状态,fg命令将后台作业提到前台运行,bg将前台作业压到后台执行。fg和bg命令的内部实现也是调用了start_job函数。打印作业信息
jobs命令可以打印当前shell下的作业信息,源码中与jobs命令相关的函数接口有:
static char * printable_job_status (j, p, format) int j;
PROCESS *p;
int format;
功能:根据作业j以及其管道p的状态成员的信息,构造可读的字符串并返回。
static int print_job (job, format, state, job_index) JOB *job;
int format, state, job_index;
功能:按照format格式,,调用pretty_print_job往stdout打印出jobindex指定的处于state状态的作业。
static void print_pipeline (p, job_index, format, stream) PROCESS *p;
int job_index, format;
FILE *stream;
功能:遍历p所在管道中的每一个进程,根据format规定的格式打印出进程的信息。如果job_index参数的值大于0,则它表示该管道所在作业的作业号。
以下是源代码中关于format格式的描述。
JLIST_NORMAL) [1]+ Running emacs
JLIST_LONG ) [1]+ 2378 Running emacs -1 ) [1]+ 2378 emacs
JLIST_NORMAL) [1]+ Stopped ls | more
JLIST_LONG ) [1]+ 2369 Stopped ls 2367 |
more JLIST_PID_ONLY) 只打印领头进程的进程ID
JLIST_CHANGED_ONLY) 只打印状态发生了改变并且还未通知过用户的作业。
static void pretty_print_job (job_index, format, stream) ;
int job_index,format;
FILE *stream;
功能:向stream文件中按照format格式打印job_index
号作业的信息,其中的管道信息调用print_pipeline
打印。
void describe_pid (pid) pid_t pid;
功能:打印可读的,以pid为领头进程的作业的作业号和进程ID信息。
void list_one_job (job, format, ignore, job_index) JOB *job;
int format, ignore, job_index;
功能:调用pretty_print_job (job_index, format, stdout);打印一个作业的相关信息。
void list_stopped_jobs (format) int format;
功能:打印所有处于停止状态的作业的信息。
void list_running_jobs (format) int format;
功能:打印所有处于运行状态的作业的信息。
void list_all_jobs (format) int format;
功能:打印所有作业的信息,如果format非0,则打印长格式信息,即详细信息,否则只打印短信息。
3.创建子进程:
创建子进程本质上也是调用fork库函数,但是shell中对其进行了几层封装以处理不同的情况。make_child函数封装了fork函数来创建子进程。make_child不仅用在作业控制中,在执行shell命令需要创建子进程时也是调用该函数进行。
以下是make_child函数的注释:
pid_t make_child (command, async_p) char *command;
int async_p;
功能:执行fork库函数产生子进程,处理相应的错误,返回子进程的进程号。
核心代码注释:
/*调用making_children函数设置already_making_children的值并且调用start_pipeline初始化当前管道变量the_pipeline的值*/
making_children ();
/*执行fork操作,并且如果是EAGAIN错误则允许重试3次*/
while ((pid = fork ()) < 0 && errno == EAGAIN && forksleep < FORKSLEEP_MAX)
{
/* 如果一直不能创建子进程,则现场时清楚一些死亡的老进程 */
waitchld (-1, 0);
sys_error ("fork: retry");
if (sleep (forksleep) != 0)
break;
forksleep <<= 1;/*将forksleep的值翻倍,*/
}
if (pid < 0)
{/*创建失败的情况*/
sys_error ("fork");/*打印错误信息*/
/*终止当前管道已构建的所有进程*/
terminate_current_pipeline ();
/*删除掉当前管道进程组占用的内存*/
if (the_pipeline)
kill_current_pipeline ();
last_command_exit_value = EX_NOEXEC;/*设置退出码*/
throw_to_top_level (); /* 跳转到上层。*/
}
if (pid == 0)
{/*在子进程中*/
mypid = getpid ();/*获取其实际进程ID*/
#if defined (BUFFERED_INPUT)
/*合理的关闭default_buffered_input*/
unset_bash_input (0);
#endif /* */
/* 恢复top-level 信号的掩码 */
sigprocmask (SIG_SETMASK, &top_level_mask, (sigset_t *)NULL);
if (job_control)
{ /*如果job_control 非0*/
if (pipeline_pgrp == 0) /* 说明这是领头进程 */
pipeline_pgrp = mypid;/**/
/* 检查是否是shell进程组本身 */
if (pipeline_pgrp == shell_pgrp)
ignore_tty_job_signals ();/*忽略SIGTSTP/SIGTTIN/SIGTTOU信号*/
else
default_tty_job_signals ();/*设置SIGTSTP/SIGTTIN/SIGTTOU信号的处理函数为默认处理*/
if (setpgid (mypid, pipeline_pgrp) < 0)/*设置mypid进程属于pipeline_pgrp 进程组*/
sys_error (_("child setpgid (%ld to %ld)"), (long)mypid, (long)
pipeline_pgrp); /*如果当前进程是领头进程,则在创建完其他进程前,调用pipe_read (pgrp_pipe)阻塞它*/
if (pipeline_pgrp == mypid)
pipe_read (pgrp_pipe);
else /* job_control为0时的工作,*/
{
if (pipeline_pgrp == 0)
pipeline_pgrp = shell_pgrp;/*设置进程组ID*/
default_tty_job_signals ();/*设置信号的默认处理方式*/
}
}
else/*在父进程中*/
{
if (first_pid == NO_PID)/*说明是父进程的第一个子进程*/
first_pid = pid;
else if (pid_wrap == -1 && pid < first_pid)
pid_wrap = 0;/*说明进程号已经用完到最大值重新从小开始计算了*/
else if (pid_wrap == 0 && pid >= first_pid)
pid_wrap = 1;
if (job_control)
{
if (pipeline_pgrp == 0)
{/*设置组进程ID*/
pipeline_pgrp = pid;
}
setpgid (pid, pipeline_pgrp);
}
else
{
if (pipeline_pgrp == 0)
pipeline_pgrp = shell_pgrp;
}
add_process (command, pid);/*将当前进程添加到当前管道进程组中*/
return (pid);/*返回子进程的ID号*/
make_child函数执行流程图:
4.wait调用
shell中对经常与fork搭配使用的wait库函数也进行了封装。shell本身也提供了wait命令。
相关的函数和宏定义有
# define WAITPID(pid, statusp, options)
封装waitpid或者wait3函数等待子进程结束。
static int waitchld (wpid, block) pid_t wpid; int block;
功能:消除死亡的或者终止的子进程,内部调用宏pid = WAITPID (-1, &status, waitpid_flags)
;实现,该宏封装了waitpif
库函数,第一个参数为-1,表示等待任何一个子进程的结束即可(相当于wait库函数)。对所有终止的子进程,调用可能存在的SIGCHLD信号处理函数。因为子进程退出,必然会发出SIGCHLD的信号。
int wait_for_single_pid (pid) pid_t pid;
功能:等待进程号为pid的子进程执行完毕。如果pid标示的子进程不是shell的子进程,则打印出错信息。如果执行失败,返回-1.如果pid子进程不存在与作业数组jobs中,则返回127.否则调用wait_for(pid)
等待其执行返回并返回wait_for
的调用结果。
void wait_for_background_pids ()
功能:等待当前shell下的所有后台进程执行完毕。
int wait_for (pid) pid_t pid;
功能:等到pid号的子进程执行完毕,返回其执行状态。如果pid号金成没有找到则返回127。函数体重调用waitchld
函数等待子进程退出,如果waitchld返回-1,则表示没有需要等待执行退出的子进程。
int wait_for_job (job) int job;
功能:等待job号作业的结束,本质上通过调用wait_for等待该作业中最后一个进程的结束实现,返回wait_for的调用结果,出错返回-1。
5.封装和调用层次见下图:
kill调用类似于wait,shell也提供了kill命令用来向进程或作业发送信号,kill命令本身也是封装的kill库函数。
shell源码中相关的库函数有
int killpg (pgrp, sig) pid_t pgrp; int sig;
{
return (kill (-pgrp, sig));
}
封装了kill库函数,向-pgrp值的进程发送sig信号。
void kill_current_pipeline ()
功能:结束并丢弃掉当前管道进程组。
int kill_pid (pid, sig, group) pid_t pid; int sig, group;
功能:向pid号进程发送sig信号,如果group值非0,则向pid所在的进程组都发送sig信号。内部调用kill库函数实现。其中killpg函数被shell中很多其他函数调用用于向进程或作业发送信号。kill_pid内部调用killpg或者kill库函数,kill_pid是kill命令主要调用的功能函数。
6. 信号控制机制
主要相关文件:
sig.h
sig.c
siglist.h
siglist.c
signames.h
support/signames.c
cwru/misc/sigs.c
cwru/misc/sigstat.c
trap.h
trap.c
其中,siglist.h、siglist.c、signames.h、support/signames.c四个文件主要封装和实现了sys_siglist以及signal_names数组。sig.h、sig.c定义了shell中信号处理和初始化的一些操作接口函数. cwru/misc/sigs.c和cwru/misc/sigstat.c提供了打印当前信号的处理方式和状态的一些借口。trap.h和trap.c文件则提供了shell内置命令trap操作相关的一些接口和数据结构。
主要数据结构:
struct termsig {
int signum;/*信号的值*/
SigHandler *orig_handler;/*如果该值非空,则是对该信号的处理函数*/
int orig_flags;/*标记位,作用尚不明*/
};
描述一个能导致shell终止的信号。SIGHUP,SIGINT都是这类型号。
static struct termsig terminating_signals[] = {
#ifdef SIGHUP
{ SIGHUP, NULL_HANDLER, 0 },
#endif
#ifdef SIGINT
{ SIGINT, NULL_HANDLER, 0 },
#endif
#ifdef SIGILL
{ SIGILL, NULL_HANDLER, 0 },
#endif
#ifdef SIGTRAP
{ SIGTRAP, NULL_HANDLER, 0 },
#endif
#ifdef SIGIOT
{ SIGIOT, NULL_HANDLER, 0 },
#endif
#ifdef SIGDANGER
{ SIGDANGER, NULL_HANDLER, 0 },
#endif
#ifdef SIGEMT
{ SIGEMT, NULL_HANDLER, 0 },
#endif
#ifdef SIGFPE
{ SIGFPE, NULL_HANDLER, 0 },
#endif
#ifdef SIGBUS
{ SIGBUS, NULL_HANDLER, 0 },
#endif
#ifdef SIGSEGV
{ SIGSEGV, NULL_HANDLER, 0 },
#endif
#ifdef SIGSYS
{ SIGSYS, NULL_HANDLER, 0 },
#endif
#ifdef SIGPIPE
{ SIGPIPE, NULL_HANDLER, 0 },
#endif
#ifdef SIGALRM
{ SIGALRM, NULL_HANDLER, 0 },
#endif
#ifdef SIGTERM
{ SIGTERM, NULL_HANDLER, 0 },
#endif
#ifdef SIGXCPU
{ SIGXCPU, NULL_HANDLER, 0 },
#endif
#ifdef SIGXFSZ
{ SIGXFSZ, NULL_HANDLER, 0 },
#endif
#ifdef SIGVTALRM
{ SIGVTALRM, NULL_HANDLER, 0 },
#endif
#if 0
#ifdef SIGPROF
{ SIGPROF, NULL_HANDLER, 0 },
#endif
#endif
#ifdef SIGLOST
{ SIGLOST, NULL_HANDLER, 0 },
#endif
#ifdef SIGUSR1
{ SIGUSR1, NULL_HANDLER, 0 },
#endif
#ifdef SIGUSR2
{ SIGUSR2, NULL_HANDLER, 0 },
#endif
};
维护了所有可以导致shell退出执行的信号的termsig数组,这样shell就可以不用对这一类信
号中的每一个都设置处理函数,可以对它们采用同样的处理机制。
char *sys_siglist[NSIG];
保存Linux下64个信号的名称的列表。下标对应信号值。名称是类似这样的值sys_siglist[SIGINT] = _("Interrupt");
、char *signal_names[NSIG + 4]
定义了存储所有64个Linux信号名字的数组。名字是类似这样的值。signal_names[SIGINT] = "SIGINT";
static int sigmodes[BASH_NSIG];
保存所有系统信号和3个shell自定义信号的状态标记的数组。可能的标记有
#define SIG_INHERITED 0x0 /* 继承自父进程的值 */
#define SIG_TRAPPED 0x1 /*当前被trapped状态*/
#define SIG_HARD_IGNORE 0x2 /* 信号在shell下被忽略*/
#define SIG_SPECIAL 0x4 /* 特殊处理的信号 */
#define SIG_NO_TRAP 0x8 /*该信号不能被trap */
#define SIG_INPROGRESS 0x10 /* 当前正在处理该信号 */
#define SIG_CHANGED 0x20 /* 在trap过程中改变了trap的值 */
#define SIG_IGNORED 0x40 /* 当前信号被忽略*/
char *trap_list[BASH_NSIG];
保存每一种信号被trap之后需要执行的工作的字符串值的数组。
7.信号控制的接口
其中,发送信号的机制已经在前面进程控制里有分析,即使用封装了kil库函数的kill命令实现。
如下一些接口是与信号处理相关的函数,即决定收到信号后应该做何种操作。
static void initialize_shell_signals ()
功能:初始化shell的信号控制。
void initialize_signals (reinit) int reinit;
功能:调用 initialize_shell_signals ()和 initialize_job_signals ()函数初始化shell的信号控制,如果reinit参数为0,则还要调用initialize_siglist ();函数初始化sys_siglist数组。
void initialize_terminating_signals ()
功能:初始化终止信号数组terminating_signals,统一的为其中的信号赋予处理函数
termsig_sighandler。实际功能调用sigaction函数实现。
sighandler termsig_sighandler (sig) int sig;
功能:先判断是否在处理sig信号前已经收到两次sig信号,如果是,则将terminate_immediately置为1表示需要立即终止,然后调用termsig_handler处理sig信号。
void termsig_handler (sig) int sig;
功能:处理值为sig的终止信号。
核心代码注释:
/*如果是SIGINT信号,则执行该信号的trap函数*/
if (sig == SIGINT && signal_is_trapped (SIGINT))
run_interrupt_trap ();
/*历史保存工作*/
if (interactive_shell && sig != SIGABRT)
maybe_save_shell_history ();
/*如果是SIGHUP信号,则调用hangup_all_jobs 挂起所有的作业*/
if (sig == SIGHUP && (interactive || (subshell_environment & (SUBSHELL_COMSUB|
SUBSHELL_PROCSUB))))
hangup_all_jobs ();
end_job_control ();
/*最后处理退出工作,对sig信号执行其默认的处理方式*/
run_exit_trap ();
set_signal_handler (sig, SIG_DFL);
kill (getpid (), sig);
sigprocmask (operation, newset, oldset) int operation, *newset, *oldset;
功能:在newset参数的信号集上执行operation操作,将执行的结果保存在oldset中。
SigHandler * set_signal_handler (sig, handler) int sig; SigHandler *handler;
功能:将sig信号的处理方式设置为handler,返回handler的指针。
void sigstat(sig) int sig;
功能:打印出sig号信号的状态,分别有被阻塞,被忽略,系统默认处理方式和被trap4种状态。
7.TRAP操作
特别的,shell中提供了内部命令trap来控制信号。Trap命令的用法简介如下:
trap捕捉到信号之后,可以有三种反应方式:
(1)执行一段程序来处理这一信号
(2)接受信号的默认操作
(3)忽视这一信号
trap对上面三种方式提供了三种基本形式:
第一种形式的trap命令在shell接收到signal list
清单中数值相同的信号时,将执行双引号中的命令串。trap "commands" signal-list
为了恢复信号的默认操作,使用第二种形式的trap命令:trap signal-list
第三种形式的trap命令允许忽视信号trap " " signal-list
shell中trap.c与trap.h文件虽然不是trap命令的直接内部实现,但是提供了许多函数
接口和数据结构供trap命令使用。其中
char *trap_list[BASH_NSIG];
保存每一种信号被trap之后需要执行的工作的字符串值的数组。该值就是上面trap命令用法中双引号内要执行的命令的值或者DEFAULT_SIG(标志按照系统默认处理该信号)、
IGNORE_SIG(标志忽略该信号)、IMPOSSIBLE_TRAP_HANDLER(处理方式还未设置时的值)
相关的函数接口有:
static void change_signal (sig, value) int sig; char *value;
功能:将trap_list[sig]
的值赋为value,并根据value是否为IGNORE_SIG设置sigmodes[sig]
标记。在判断当前是否在trap调用中,即sigmodes[sig] & SIG_INPROGRESS是否为真,如果为真,则需要标记sigmodes[sig] |= SIG_CHANGED;来标记其值的变化。
static void get_original_signal (sig) int sig;
功能:对每一个信号调用GET_ORIGINAL_SIGNAL宏将其的trap处理方式设置为系统默认的处理方式。
static int _run_trap_internal (sig, tag) int sig; char *tag;
功能:当shell中提供了对sig信号的trap处理并且该信号未被设置忽略标记时,运行sig信号的trap处理操作。
核心代码注释:
/*只有满足如下if条件的信号才执行trap操作*/
if ((sigmodes[sig] & SIG_TRAPPED) && ((sigmodes[sig] & SIG_IGNORED) == 0) &&
(trap_list[sig] != (char *)IMPOSSIBLE_TRAP_HANDLER) &&
((sigmodes[sig] & SIG_INPROGRESS) == 0))
{
/*以下工作为在执行trap操纵前保存一些程序当前的状态值*/
old_trap = trap_list[sig]; /*保存sig信号要做的trap工作*/
sigmodes[sig] |= SIG_INPROGRESS;/*标记当前开始trap操作了*/
sigmodes[sig] &= ~SIG_CHANGED; /* 确保SIG_CHANGED位不成立*/
trap_command = savestring (old_trap); /*为了执行相应的操作,为相应的命令申请内存地址保存在trap_command中*/
running_trap = sig + 1;/*设置running_trap的值*/
trap_saved_exit_value = last_command_exit_value;/*保存执行该trap操作前最后一次命令执行的返回值*/
ps = save_pipestatus_array ();/*保存shell变量PIPESTATUS的值*/
token_state = save_token_state ();/*保存当前语法分析的状态*/
save_subst_varlist = subst_assign_varlist;/*保存subst_assign_varlist的值*/
subst_assign_varlist = 0;
/*如果当前正运行于一个函数中,则如下代码将能处理捕获返回值并跳转的情况*/
save_return_catch_flag = return_catch_flag;
if (return_catch_flag)
{
COPY_PROCENV (return_catch, save_return_catch);
function_code = setjmp (return_catch);
}
/*正确设置parse_and_execute的值,并且调用该函数解析并执行trap操作*/
flags = SEVAL_NONINT|SEVAL_NOHIST;
if (sig != DEBUG_TRAP && sig != RETURN_TRAP && sig != ERROR_TRAP)
flags |= SEVAL_RESETLINE;
if (function_code == 0)
parse_and_execute (trap_command, tag, flags);
/*执行完毕后,以下操作恢复之前保存的一些程序状态值*/
restore_token_state (token_state);
free (token_state);
subst_assign_varlist = save_subst_varlist;
trap_exit_value = last_command_exit_value;
last_command_exit_value = trap_saved_exit_value;
running_trap = 0;
sigmodes[sig] &= ~SIG_INPROGRESS;
/*处理函数返回值需要跳转的情况*/
if (save_return_catch_flag)
{
return_catch_flag = save_return_catch_flag;
return_catch_value = trap_exit_value;
COPY_PROCENV (save_return_catch, return_catch);
if (function_code)
longjmp (return_catch, 1);
}
return trap_exit_value;/*返回trap操作的结果值*/
}
附:部分重要源码文件简介
文件(或文件夹) | 说明 |
---|---|
shell.h | shell.c对应的头文件,共171行 |
shell.c | main()函数的所在,定义了shell启动和运行过程中的一些状态量,依据不同的启动参数、环境变量等来初始化shell的工作状态,共1856行 |
Eval.c | 读取并解释执行shell命令。共281行 |
Command.h | 定义了表示命令的数据结构,声明了创建命令的函数,共387行 |
Copy_cmd.c | 定义了复制命令的一系列函数,代码共450行。 |
Execute_cmd.c | 定义了执行一个命令需要的相关函数。提供给外部的调用接口函数是execute_command(),该函数调用execute_command_internal()执行命令。针对不同类型的命令(控制结构、函数、算术等),execute_command_internal()调用对应的内部函数来完成相应功能。其中execute_builtin()执行内部命令;execute_disk_command()执行外部文件。execute_disk_command()通过调用jobs.c或nojobs.c中的make_child()来执行新进程的fork操作。代码共5187行。 |
Variables.h | 本文件定义了描述shell变量要用到的一些数据结构,代码共390行。 |
Variables.c | 本文件处理了操作shell中的各种变量的函数集,bash中的变量不强调类型,可以认为都是字符串。代码共4793行。 |
Make_cmd.c | 构造各类命令、复位向等语法结构实例所需的函数。由yacc语法分析器、redir.c等调用,代码共888行。 |
dispose_cmd.c | 释放一个命令结构所占用的内存,代码共342行。 |
jobs.h | 声明和定义了jobs.c以及作业控制需要的数据结构和函数,代码共250行。 |
jobs.c | 本文件是作业控制的主要实现文件,主要入口是make_child(),该函数封装了fork库函数用来创建进程并执行。jobs、fg、bg、kill等命令的内部实现都在这里。代码共4276行。 |
sig.h | 声明了sig.c的一些控制信号处理的函数,定义了一些相关的宏。代码共137行。 |
sig.c | 定义了shell中信号处理和初始化的一些操作接口函数,代码共677行 |
siglist.h | 封装了sys_siglist可能的几种定义的文件,代码共44行。 |
siglist.c | 因为有些系统没有_sys_siglist变量来保存信号信息列表,因此提供该文件构造一个sys_siglist数组。代码共229行。 |
signames.h | 本文件内容由mksignames.c编译生成的二进制文件执行生成,用于定义保存了所有信号名称的数组signal_names以及定义初始化该数组内容的宏 initialize_signames(),代码共78行。 |
support/signames.c | 定义了signal_names数组,定义了初始化该数组的函数initialize_signames。代码共401行。 |
cwru/misc/sigs.c | 打印出当前shell对于每一个信号的处理方式,代码共45行。 |
cwru/misc/sigstat.c | 打印出所有信号的状态,比如是否被阻塞,是否被忽略,是否被trap,是否采用默认处理方式等等,代码共226行。 |
trap.h | 定义了trap机制中用到的一些数据结构,声明了trap.c文件的函数,代码共105行。 |
trap.c | 操作trap命令所需的一些对象的函数。非trap命令的实现,只是提供了一些trap命令需要用到的接口,trap命令的实现在trap.def文件中。代码共1121行。 |
Shell主要逻辑源码级分析 (2)——SHELL作业控制的更多相关文章
- Shell主要逻辑源码级分析(1)——SHELL运行流程
版权声明:本文由李航原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/109 来源:腾云阁 https://www.qclou ...
- TaskTracker任务初始化及启动task源码级分析
在监听器初始化Job.JobTracker相应TaskTracker心跳.调度器分配task源码级分析中我们分析的Tasktracker发送心跳的机制,这一节我们分析TaskTracker接受JobT ...
- MapReduce的ReduceTask任务的运行源码级分析
MapReduce的MapTask任务的运行源码级分析 这篇文章好不容易恢复了...谢天谢地...这篇文章讲了MapTask的执行流程.咱们这一节讲解ReduceTask的执行流程.ReduceTas ...
- MapReduce的MapTask任务的运行源码级分析
TaskTracker任务初始化及启动task源码级分析 这篇文章中分析了任务的启动,每个task都会使用一个进程占用一个JVM来执行,org.apache.hadoop.mapred.Child方法 ...
- 监听器初始化Job、JobTracker相应TaskTracker心跳、调度器分配task源码级分析
JobTracker和TaskTracker分别启动之后(JobTracker启动流程源码级分析,TaskTracker启动过程源码级分析),taskTracker会通过心跳与JobTracker通信 ...
- TableInputFormat分片及分片数据读取源码级分析
我们在MapReduce中TextInputFormat分片和读取分片数据源码级分析 这篇中以TextInputFormat为例讲解了InputFormat的分片过程以及RecordReader读取分 ...
- MapReduce job在JobTracker初始化源码级分析
mapreduce job提交流程源码级分析(三)中已经说明用户最终调用JobTracker.submitJob方法来向JobTracker提交作业.而这个方法的核心提交方法是JobTracker.a ...
- Flume-NG(1.5版本)中SpillableMemoryChannel源码级分析
SpillableMemoryChannel是1.5版本新增的一个channel.这个channel优先将evnet放在内存中,一旦内存达到设定的容量就使用file channel写入磁盘.然后读的时 ...
- Apache Log4j 远程代码执行漏洞源码级分析
漏洞的前因后果 漏洞描述 漏洞评级 影响版本 安全建议 本地复现漏洞 本地打印 JVM 基础信息 本地获取服务器的打印信息 log4j 漏洞源码分析 扩展:JNDI 危害是什么? GitHub 项目 ...
随机推荐
- 4G模块ME3760_V2的拨号过程
AT AT+CFUN=1 模块功能全打开,上电可以设置默认状态 AT+ZSET="LET_INFO" 掉电后可以保存AT+CFUN ...
- HISTTIMEFORMAT 设置历史命令时间的格式
echo 'HISTTIMEFORMAT="%F %T `whoami`" ' >>/etc/bashrc whoami 完了后面要有空格不然会连住和命令 ===== ...
- find 命令一个命令多参数如何使用,????,perm
[root@ob2 mytmp]# find -mtime -7 -type f \( -name "*.html" -o -name "*.tar.gz" \ ...
- WCF客户端获取服务器返回数据报错
错误信息:An error occurred while receiving the HTTP response to http://127.0.0.1/SIHIS/Infection/PubExec ...
- RTMP流媒体播放过程:握手,建立连接,建立流,播放
本文讲述从打开一个RTMP流媒体到视音频数据开始播放的整个过程. 播放一个流媒体有两个前提步骤: 第一步,建立一个网络连接(NetConnection): 第二步,建立一个网络流(NetStream) ...
- ubuntu rails3.2.16 提示gem mysql adapter
把database.yml的adapter改为mysql2 把Gemfile文件中的gem mysql改为gem mysql2
- stm32独立看门狗
转载:http://blog.sina.com.cn/s/blog_7f1dc53b01010mqa.html 实验现象: 开始LED1亮,LED2熄灭,若不隔时间按KEY1则发现LED2因独立看门狗 ...
- laravel 参数配置
1:在项目根目录下有个叫.env的文件.这个文件是配置配置.由config文件下的app.php 直接读取. #参数解释 APP_ENV=local #访问地址 APP_DEBUG=true #是否开 ...
- View与Model绑定注意事项 (视图无数据显示)
Qt 中视图与模型绑定时,模型必须使用new来创建.否则刚开始初始化的时候,视图无数据显示,或者后期视图不能随着模型的改变而改变. 具体原因:我猜测是局部变量生命周期的问题.new 的变量在堆中,除非 ...
- 关于Android中Fragment静态和动态加载的方法
一.静态加载 1.首先创建一个layout布局fragment.xml,里面放要显示和操作的控件 2.创建一个layout布局main1.xml,用来实现页面的跳转(跳转为要实现静态加载的界面) 3. ...