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

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

从重复shell看内核创建及shell的单&操作  

2013-07-11 00:40:00|  分类: Linux系统编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、原因
今天写了个简单的脚本,大致的意思是想在后台执行一个命令,执行一个多环境下的并发功能。写个例子就是
cat somefile | grep -ve '^$' |
for file in filelist
do
somescript.sh  & >$file.log 2>&1
done
本来的意思是想在后台执行somescript.sh,并且把日志定位到对应的文件中。但是执行了之后,通了马蜂窝了。万箭齐发,很多命令瞬间迸发出来,关都关不掉。更坑爹的是使用ps一看,系统中派生的脚本数量是预期数量的一倍。
套用当前流行的那句话说:我和我的小伙伴们都惊呆了。但是在镇定了之后,我们还是冷静的分析了一下原因,感觉还是挺有意思的,所以在这里总结一下。
二、bash中单 & 的意义
static int
execute_connection (command, asynchronous, pipe_in, pipe_out, fds_to_close)
     COMMAND *command;
     int asynchronous, pipe_in, pipe_out;
     struct fd_bitmap *fds_to_close;
{
  REDIRECT *rp;
  COMMAND *tc, *second;
  int ignore_return, exec_result, was_error_trap, invert;
  volatile int save_line_number;

  ignore_return = (command->flags & CMD_IGNORE_RETURN) != 0;

  switch (command->value.Connection->connector)
    {
    /* Do the first command asynchronously. */
    case '&':
      tc = command->value.Connection->first;
      if (tc == 0)
    return (EXECUTION_SUCCESS);

      rp = tc->redirects;

      if (ignore_return)
    tc->flags |= CMD_IGNORE_RETURN;
      tc->flags |= CMD_AMPERSAND;

      /* If this shell was compiled without job control support,
     if we are currently in a subshell via `( xxx )', or if job
     control is not active then the standard input for an
     asynchronous command is forced to /dev/null. */
#if defined (JOB_CONTROL)
      if ((subshell_environment || !job_control) && !stdin_redir)
#else
      if (!stdin_redir)
#endif /* JOB_CONTROL */
    tc->flags |= CMD_STDIN_REDIR;

      exec_result = execute_command_internal (tc, 1, pipe_in, pipe_out, fds_to_close);
      QUIT;

      if (tc->flags & CMD_STDIN_REDIR)
    tc->flags &= ~CMD_STDIN_REDIR;

      second = command->value.Connection->second;
      if (second)
    {
      if (ignore_return)
        second->flags |= CMD_IGNORE_RETURN;

      exec_result = execute_command_internal (second, asynchronous, pipe_in, pipe_out, fds_to_close);
    }

      break;

    /* Just call execute command on both sides. */
    case ';':
      if (ignore_return)
    {
      if (command->value.Connection->first)
        command->value.Connection->first->flags |= CMD_IGNORE_RETURN;
      if (command->value.Connection->second)
        command->value.Connection->second->flags |= CMD_IGNORE_RETURN;
    }
      executing_list++;
      QUIT;
      execute_command (command->value.Connection->first);
      QUIT;
      exec_result = execute_command_internal (command->value.Connection->second,
                      asynchronous, pipe_in, pipe_out,
                      fds_to_close);
      executing_list--;
      break;

    case '|':
      was_error_trap = signal_is_trapped (ERROR_TRAP) && signal_is_ignored (ERROR_TRAP) == 0;
      invert = (command->flags & CMD_INVERT_RETURN) != 0;
      ignore_return = (command->flags & CMD_IGNORE_RETURN) != 0;

      line_number_for_err_trap = line_number;
      exec_result = execute_pipeline (command, asynchronous, pipe_in, pipe_out, fds_to_close);

      if (was_error_trap && ignore_return == 0 && invert == 0 && exec_result != EXECUTION_SUCCESS)
    {
      last_command_exit_value = exec_result;
      save_line_number = line_number;
      line_number = line_number_for_err_trap;
      run_error_trap ();
      line_number = save_line_number;
    }

      if (ignore_return == 0 && invert == 0 && exit_immediately_on_error && exec_result != EXECUTION_SUCCESS)
    {
      last_command_exit_value = exec_result;
      run_pending_traps ();
      jump_to_top_level (ERREXIT);
    }

      break;

    case AND_AND:
    case OR_OR:
      if (asynchronous)
    {
      /* If we have something like `a && b &' or `a || b &', run the
         && or || stuff in a subshell.  Force a subshell and just call
         execute_command_internal again.  Leave asynchronous on
         so that we get a report from the parent shell about the
         background job. */
      command->flags |= CMD_FORCE_SUBSHELL;
      exec_result = execute_command_internal (command, 1, pipe_in, pipe_out, fds_to_close);
      break;
    }

      /* Execute the first command.  If the result of that is successful
     and the connector is AND_AND, or the result is not successful
     and the connector is OR_OR, then execute the second command,
     otherwise return. */

      executing_list++;
      if (command->value.Connection->first)
    command->value.Connection->first->flags |= CMD_IGNORE_RETURN;

      exec_result = execute_command (command->value.Connection->first);
      QUIT;
      if (((command->value.Connection->connector == AND_AND) &&
       (exec_result == EXECUTION_SUCCESS)) ||
      ((command->value.Connection->connector == OR_OR) &&
       (exec_result != EXECUTION_SUCCESS)))
    {
      if (ignore_return && command->value.Connection->second)
        command->value.Connection->second->flags |= CMD_IGNORE_RETURN;

      exec_result = execute_command (command->value.Connection->second);
    }
      executing_list--;
      break;

    default:
      command_error ("execute_connection", CMDERR_BADCONN, command->value.Connection->connector, 0);
      jump_to_top_level (DISCARD);
      exec_result = EXECUTION_FAILURE;
    }

  return exec_result;
}
在source insight里看这个函数这么长的,贴出来感觉就不一样了。但是我们看不懂代码还看不懂注释嘛,作者说了,通过 ‘&’ 连接的命令,第一个命令是异步执行的,第二命令则单独执行,如果要让整个命令在后台执行,则需要将‘&’放在整个命令行的最后。
三、为什么看到脚本是双料的
我记得自己在之前一篇博客中说过这个问题,如果一个命令是shell的内置命令,并且它的有管道输入或者输出,此时同样需要派生一个新的进程。但是此时也只是fork一个进程,并没有执行exec。大家对这个体会可能不深,但是在后果比较严重的时候大家就知道问题的严重性了。
那么我们看一下内核中对于fork的创建,
long do_fork--->>copy_process--->>dup_task_struct
tsk = alloc_task_struct();
    if (!tsk)
        return NULL;

    ti = alloc_thread_info(tsk);
    if (!ti) {
        free_task_struct(tsk);
        return NULL;
    }

    *tsk = *orig;当然这个只是初始化,初始化之后还有各种必要的修正,所以也不用惊讶。
直接将父进程的task_struct拷贝到新分配的task_struct结构中,而这个结构中就包含了大家知道的命令行内容,
    char comm[TASK_COMM_LEN]; /* executable name excluding path
                     - access with [gs]et_task_comm (which lock
                       it with task_lock())
                     - initialized normally by flush_old_exec */
但是你以为这个就是表示我们ps axu看到的命令行吗?你太天真了,骚年。因为这个结构的长度只有16个字节,去掉一个‘\0’,只能有15个字符,这个现实的内容是通过 /proc/$pid/stat文件中展示的文件名,而通过ps现实的命令行内容则是从进程地址空间中的保存的argv数组打印出来的。ps现实的字符串来自/proc/$pid/cmdline的内容,而这个内容在内核中为
static int proc_pid_cmdline(struct task_struct *task, char * buffer)
res = access_process_vm(task, mm->arg_start, buffer, len, 0);
大家只有知道这个内容不是从task_struct中读到的就行了。
遗憾的是,对于fork之后的进程的这内容也是相同的,所以fork之后的进程在/proc/$pid/cmdline中显示的内容是相同的,进而通过ps看到的内容也是相同的。
四、还原一下现场
[root@Harry doubleinstance]# cat main.sh
#!/bin/sh
for file in *.sh
do
sh sub.sh &
done

[root@Harry doubleinstance]# cat sub.sh
echo |
for (( i=0; i< 1; i++))
do
sleep 1234
done

执行
sh main.sh
之后,系统中进程的结构
root      2067  0.0  0.1   5120  1672 pts/0    Ss   Jul10   0:00 bash
root      2812  0.0  0.1   4924  1036 pts/0    S    00:04   0:00 sh sub.sh
root      2814  0.0  0.0   4924   476 pts/0    S    00:04   0:00 sh sub.sh
root      2815  0.0  0.1   4924  1040 pts/0    S    00:04   0:00 sh sub.sh
root      2817  0.0  0.0   4924   480 pts/0    S    00:04   0:00 sh sub.sh
root      2818  0.0  0.0   3940   476 pts/0    S    00:04   0:00 sleep 1234
root      2819  0.0  0.0   3940   480 pts/0    S    00:04   0:00 sleep 1234
root      2821  3.0  0.0   4692   992 pts/0    R+   00:04   0:00 ps axu
可以看到此时sub.sh的数量是4个,而sleep进程数量为2个,
[root@Harry doubleinstance]# pstree -pa | grep sub
  |   |   |-grep,2838 sub
  |-sh,2812 sub.sh
  |   `-sh,2814 sub.sh
  |-sh,2815 sub.sh
  |   `-sh,2817 sub.sh
可以看到有两组同名的进程为父子关系,而子shell的特殊之处在于内置命令for之前有一个管道的标准输入,从而导致子shell的创建,创建之后由于只是fork而没有执行exec,所以从名字上看和父进程是一样一样的。
五、暂停终端输出及重新启动
对于一些打印型的命令,如果阻塞了它的标准输入,此时相当于进程在执行printf的时候无法返回,可以暂时禁止程序的执行,此时有缓冲的时间进行观察和决策而不是眼睁睁的看着程序不可控,
[root@Harry doubleinstance]# stty -a
speed 38400 baud; rows 24; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?;
swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc ixany imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke
和ctrl+c,ctrl+\不同,这两个组合键并不是通过发信号的形式告诉进程发生了什么事情,而是内核直接处理了这个输入,这个输入对用户是不可见,也即是说内核默默的拦截了这个输入,并且执行了相应的操作,对于read/write操作来说是不可见的。
内核中对应的两个定义类型为
#define START_CHAR(tty) ((tty)->termios->c_cc[VSTART])
#define STOP_CHAR(tty) ((tty)->termios->c_cc[VSTOP])
n_tty.c
static inline void n_tty_receive_char(struct tty_struct *tty, unsigned char c)

    if (I_IXON(tty)) {
        if (c == START_CHAR(tty)) {
            start_tty(tty);
            return;
        }
        if (c == STOP_CHAR(tty)) {
            stop_tty(tty);
            return;
        }
    }
在伪终端的暂停中,其生效位置为
static int pty_write(struct tty_struct * tty, const unsigned char *buf, int count)
{
    struct tty_struct *to = tty->link;
    int    c;


    if (!to || tty->stopped)
        return 0
;

    c = to->receive_room;
    if (c > count)
        c = count;
    to->ldisc.receive_buf(to, buf, NULL, c);
   
    return c;
}

static int pty_write_room(struct tty_struct *tty)
{
    struct tty_struct *to = tty->link;

    if (!to || tty->stopped)
        return 0
;

    return to->receive_room;
}
static void __init unix98_pty_init(void)
tty_set_operations(ptm_driver, &pty_ops);
下面代码属于猜测部分,未验证:
阻塞位置
static ssize_t write_chan(struct tty_struct * tty, struct file * file,
              const unsigned char * buf, size_t nr)

        if (O_OPOST(tty) && !(test_bit(TTY_HW_COOK_OUT, &tty->flags))) {
            while (nr > 0) {
                ssize_t num = opost_block(tty, b, nr);
                if (num < 0) {
                    if (num == -EAGAIN)
                        break;
                    retval = num;
                    goto break_out;
                }
                b += num;
                nr -= num;
                if (nr == 0)
                    break;
                c = *b;
                if (opost(c, tty) < 0)
                    break
; 跳出循环,外层还有一个更大的循环在等待着这个break,所以还是没有逃出如来的五指山
                b++; nr--;
            }
            if (tty->driver->flush_chars)
                tty->driver->flush_chars(tty);
        }

static int opost(unsigned char c, struct tty_struct *tty)
{
    int    space, spaces;

    space = tty->driver->write_room(tty);
    if (!space)
        return -1;
  评论这张
 
阅读(1101)| 评论(0)
推荐 转载

历史上的今天

评论

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

页脚

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