注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

bash time实现  

2016-09-23 20:22:16|  分类: bash分析 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、内置的time功能
bash内置了一个time功能,说它是内置功能,是由于它不以外bash之外的二进制实现。和通常的bash命令一样,独立于bash之外,系统也可能存在有独立的time二进制,只是这个二进制并不影响bash使用内置time功能。关于这一点可以通过下面的功能简单测试:
tsecer@harry: which time
/usr/bin/time
tsecer@harry: mv /usr/bin/time /usr/bin/time.backy
tsecer@harry: time sleep 1

real 0m1.010s
user 0m0.000s
sys 0m0.007s
tsecer@harry: which time
/usr/bin/which: no time in (/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/bin:/bin:/root/bin)
tsecer@harry: 
可以看到即使不存在time二进制,统计进程运行时间的功能依然可以使用。
二、bash的语法解析
bash-4.1\parse.y
timespec: TIME
{ $$ = CMD_TIME_PIPELINE; }
| TIME TIMEOPT
{ $$ = CMD_TIME_PIPELINE|CMD_TIME_POSIX; }
;
pipeline_command: pipeline
……
| timespec pipeline
……
pipeline: pipeline '|' newline_list pipeline
{ $$ = command_connect ($1, $4, '|'); }
这里看到的是TIME的优先级比较低,低于管道的优先级。当然这只是从语法文件看到的一个属性,具体这个特性有什么意义现在还想不到。下面的例子展示了time的优先级要低于管道的优先级。                        
tsecer@harry: time sleep 1 | sleep 2

real    0m2.009s
user    0m0.004s
sys     0m0.004s
tsecer@harry: (time sleep 1) | sleep 2

real    0m1.007s
user    0m0.004s
sys     0m0.000s
tsecer@harry: 
三、bash对于此类命令的执行
bash-4.1\execute_cmd.c
execute_command_internal (command, asynchronous, pipe_in, pipe_out,
……
#if defined (COMMAND_TIMING)
  if (command->flags & CMD_TIME_PIPELINE)
    {
      if (asynchronous)
{
 command->flags |= CMD_FORCE_SUBSHELL;
 exec_result = execute_command_internal (command, 1, pipe_in, pipe_out, fds_to_close);
}
      else
{
 exec_result = time_command (command, asynchronous, pipe_in, pipe_out, fds_to_close);
#if 0
 if (running_trap == 0)
#endif
   currently_executing_command = (COMMAND *)NULL;
}
      return (exec_result);
    }
#endif /* COMMAND_TIMING */

static int
time_command (command, asynchronous, pipe_in, pipe_out, fds_to_close)
……
#if defined (HAVE_GETRUSAGE) && defined (HAVE_GETTIMEOFDAY)
#  if defined (HAVE_STRUCT_TIMEZONE)
  gettimeofday (&before, &dtz);
#  else
  gettimeofday (&before, (void *)NULL);
#  endif /* !HAVE_STRUCT_TIMEZONE */
  getrusage (RUSAGE_SELF, &selfb);
  getrusage (RUSAGE_CHILDREN, &kidsb);
#else
#  if defined (HAVE_TIMES)
  tbefore = times (&before);
#  endif
#endif
……
  difftimeval (&real, &before, &after);
……
  addtimeval (&sys, difftimeval(&after, &selfb.ru_stime, &selfa.ru_stime),
   difftimeval(&before, &kidsb.ru_stime, &kidsa.ru_stime));
这里代码比较烦琐,大致的意思是对于realtime是通过在执行命令前后的time系统调用时间差获得的,也就是前面看到的real行的输出,然后通过getrusage (RUSAGE_SELF, &selfa)获得自己的系统资源使用情况;getrusage (RUSAGE_CHILDREN, &kidsa)获得所有子进程的资源使用情况,将自己的用户态运行时间加上子进程的用户态运行时间得到user,将自己的系统态运行时间加上子进程的系统态运行时间得到sys列输出数值。在bash的实现中也可以通过支持times系统调用的接口来实现,但是对linux来说,两者底层的实现相同,所以不再赘述。不过这里要注意的一点是:在执行这些系统调用获得子进程的执行时间的时候,子进程已经退出,所以这也就意味着子进程需要在退出之前将自己的状态保存在父进程的进程控制表中。
四、进程用户态和系统态执行时间如何获得
linux-2.6.17\kernel\sched.c
x86_64在多核系统下

smp_local_timer_interrupt===>>>update_process_times(user_mode(regs))

/*
 * Called from the timer interrupt handler to charge one tick to the current 
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id();

/* Note: this timer irq context must be accounted for as well. */
if (user_tick)
account_user_time(p, jiffies_to_cputime(1));
else
account_system_time(p, HARDIRQ_OFFSET, jiffies_to_cputime(1));
run_local_timers();
if (rcu_pending(cpu))
rcu_check_callbacks(cpu, user_tick);
scheduler_tick();
  run_posix_cpu_timers(p);
}
这个统计相当于是在定时器的每个tick中判断下当前进程在执行用户态和内核态代码,也就是相当于一个抽样,并不是严格在进程切换的时候来统计进程的系统和内核的运行时间。对于x86_64,其中的#define user_mode(regs) (!!((regs)->cs & 3)),如果代码段寄存器低2位非零,则认为用户态;内核态特权级为0。
#define jiffies_to_cputime(__hz) (__hz)
void account_user_time(struct task_struct *p, cputime_t cputime)
{
struct cpu_usage_stat *cpustat = &kstat_this_cpu.cpustat;
cputime64_t tmp;

p->utime = cputime_add(p->utime, cputime);

/* Add user time to cpustat. */
tmp = cputime_to_cputime64(cputime);
if (TASK_NICE(p) > 0)
cpustat->nice = cputime64_add(cpustat->nice, tmp);
else
cpustat->user = cputime64_add(cpustat->user, tmp);
}
这来统计时增加的是cputime,也就是以HZ为单位的时间片,这就需要在给用户态时间的时候转换为通用时间
sys_getrusage==>>>getrusage==>>>k_getrusage
cputime_to_timeval(utime, &r->ru_utime);
#define cputime_to_timeval(__ct,__val) jiffies_to_timeval(__ct,__val)
tsecer@harry: cat /boot/config-3.11.10-301.fc20.x86_64 | grep HZ
CONFIG_NO_HZ_COMMON=y
# CONFIG_HZ_PERIODIC is not set
CONFIG_NO_HZ_IDLE=y
# CONFIG_NO_HZ_FULL is not set
CONFIG_NO_HZ=y
# CONFIG_RCU_FAST_NO_HZ is not set
# CONFIG_HZ_100 is not set
# CONFIG_HZ_250 is not set
# CONFIG_HZ_300 is not set
CONFIG_HZ_1000=y
CONFIG_HZ=1000
这意味着系统的精确度大致为0.001s,也就是1ms。
五、子进程时间向父进程的累加
进程退出,在被其它进程执行wait之后,会将自己的统计信息追加到父进程的cutime和cstime中,这也就是父进程获得子进程执行时间的数据源。
do_wait===>>wait_task_zombie
psig->cutime =
cputime_add(psig->cutime,
cputime_add(p->utime,
cputime_add(sig->utime,
   sig->cutime)));
psig->cstime =
cputime_add(psig->cstime,
cputime_add(p->stime,
cputime_add(sig->stime,
   sig->cstime)));
六、进程组概念
对于管道连接的命令,shell通常将他们放在同一个进程组中。那么放在同一个进程组是如何实现的?同一个进程组有什么意义?如果想把一个进程添加到一个进程组中,可以通过setpgid系统调用来完成,这个设置有一个最基本的限制,就是参数中指定的两个pid对应的金城必须存在,而不能是自定义的一个数字。而将一个进程的gid设置为一个特定的进程组,可以按照比较流行的hash概念来理解,这个是相当于将进程的一个属性设置为指定的pid,而具有相同gid的进程被hash(圈定)到特定的一组。
在之前的计算机系统中,用户使用计算机都是通过终端来登录服务器的,所以会话(session)、进程组的管理都是和终端有紧密关系。对于进程组来收,当终端上发生一个事件触发一个信号的时候,例如用户按键CTRL+C触发SIGINT信号时,这个信号是发给了整个进程组中的所有进程(而不是某个特定进程)。例如,在终端执行 ps aux | grep ps | grep -v grep的时候,管道连接的三个进程就是在相同的进程组中,当CTRL+C的时候,这三个进程均会收到SIGINT信号。所以这样的概念是一个逻辑的概念,更多是为了完成终端中前台进程组的控制。
再次回到之前的例子,对于类似于一个
sleep 1 | sleep 10
这样的终端命令,明显地,第一个进程将会早于第二个进程退出,而第二个进程并没有依赖第一个进程的输出,这个时候相当于这个进程组的leader进程提前退出了,这退出对于整个进程组有没有什么影响呢?从当前测试的例子来看,没有看到任何特殊需要注意的效果。那么紧接着的问题就是说,当bash创建了这么一串的进程组之后,这些进程按照不同的顺序次第退出,那么父进程如何知道进程组中的所有进程都已经执行结束了呢
对于这一点在bash中也并不难找到bash-4.0\jobs.c
/* waitchld() reaps dead or stopped children.  It's called by wait_for and
   sigchld_handler, and runs until there aren't any children terminating any
   more.
   If BLOCK is 1, this is to be a blocking wait for a single child, although
   an arriving SIGCHLD could cause the wait to be non-blocking.  It returns
   the number of children reaped, or -1 if there are no unwaited-for child
   processes. */
static int
waitchld (wpid, block)
     pid_t wpid;
     int block;
{
……
      pid = WAITPID (-1, &status, waitpid_flags);
……
      /* If waitpid returns -1 with errno == ECHILD, there are no more
unwaited-for child processes of this shell. */
      if (pid < 0 && errno == ECHILD)
{
 if (children_exited == 0)
   return -1;
 else
   break;
}

七、结论
bash通过time获得的系统执行时间和用户执行时间并不是严格意义上的“运行时间”,而更像是一个抽样结果,但是通常误差不大,这一点和内核态profile工具oprofile的实现原理类似。但是real是通过在执行命令前后gettimeofday系统调用返回的系统时间差,所以这个时间通常来说是我们关系的时间。

  评论这张
 
阅读(27)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017