wshd进程分析

warden是cloudfoundry中比较核心的应用程序,负责应用程序的资源限制和隔离,而wshd是warden里面最重要的一个程序,它负责对发送给容器的指令的执行,在创建一个容器的时候会启动wshd这个进程。

首先上张图,wshd总体结构


Main函数首先执行parent_run函数,parent_run里面的执行顺序如下:

  * Hook-parent-before-clone.sh
  * child_start函数
  * hook-parent-after-clone.sh

1,hook_parent_before-clone.sh

function setup_fs()  {
  mkdir -p tmp/rootfs mnt
  if should_use_aufs; then
   mount -n -t aufs -o br:tmp/rootfs=rw:$rootfs_path=ro+wh none mnt
  else if  should_use_overlayfs; then
   mount -n -t overlayfs -o rw,upperdir=tmp/rootfs,lowerdir=$rootfs_path none mnt
  else
    setup_fs_other
  fi
}
将sketlon拷贝纸容器所在的目录,然后创建mnt以及tmp/rootfs文件夹,根据操作系统,ubutu 10.04选择aufs,12.04选择overlayfs。将tmp/rootfs和/tmp/warden/rootfs组合成为一个文件系统,挂载在mnt目录下。(cd进mnt,目前看不到合并后的文件系统,后面有一个mount /mnt操作,不知道是否为这原因,文件系统还需进一步分析)

2,Hook_parent_after_clone.sh

修改cgroup目录下面,
修改cpuset,cpuset.mems等参数,修改实例可以访问的设备device.allow,最重要的是把child_start启动之后返回的PID
echo $PID > $instance_path/tasks,这个wshd进程代表了一个容器。

ip link add name $network_host_iface type veth peer name $network_container_iface
ip link set $network_host_iface netns 1
ip link set $network_container_iface netns $PID
ifconfig $network_host_iface $network_host_ip netmask $network_netmask
上面用ip link add 添加了一对虚拟网卡,将network_host_iface放到defalut network namespace中,将另外一张网卡放入到wshd这个进程的network namspace中,也就是容器的network namespace中。

3,child_start函数

Child_start负责去启动一个wshd进程,child_start clone出一个子进程,代码如下:
* Setup namespaces */
  flags |= CLONE_NEWIPC;
  flags |= CLONE_NEWNET;
  flags |= CLONE_NEWNS;
  flags |= CLONE_NEWPID;
  flags |= CLONE_NEWUTS;

  pid = clone(child_run, stack, flags, w); //create a child process ,function child_run,parameter is w
为这个新的进程创建新的IPC,NET,Mount,PID,UTS命名空间。

4,child_run:

    hook-before-child-pivot.sh
    pivot_root
    hook_after_child_pivot.sh
    exec(child_continue)
    child_loop

4.1 hook-before-child-pivot.sh:

这里面没有任何操作,空的

4.2 pivot_root:

pivot_root是一个类似于chroot,用于change root directory,在pivot_root时候,创建了一个mnt目录,把旧的文件系统挂载到mnt目录下,chroot默认启动的是/bin/bash,但是对于容器来说,在sketlon目录的bin目录下面仅有wshd,wsh,imux_link,iomux_spawn这四个执行程序,如果你自己要测试,进入容器内,那么而需要sudo chroot . bin/wsh

4.3 hook_after_child_pivot.sh:

准备虚拟终端虚拟终端需要的ptmx以及pts
mkdir -p /dev/pts
mount -t devpts -o newinstance,ptmxmode=0666 devpts /dev/pts //在dev/pts下游ptmx这个文件
ln -sf pts/ptmx /dev/ptmx

接着挂在proc文件系统
mkdir -p /proc
mount -t proc none /proc

配置容器的主机名,启动容器内的网卡并且配置这张网卡,添加一条缺省的路由,网络主机IP和网络容器接口。
hostname $id
ifconfig lo 127.0.0.1
ifconfig $network_container_iface $network_container_ip netmask $network_netmask mtu $container_iface_mtu
route add default gw $network_host_ip $network_container_iface

4.4 exec(child_continue):

//继续调用wshd,但是传入的参数--continue,调用child_continue
  execl("/sbin/wshd", "/sbin/wshd", "--continue", NULL);

4.5 child_continue:

  w = child_load_from_shm();//读取父进程放在共享内存中的wshd_t结构。

  rv = mount_umount_pivoted_root("/mnt");//umount原来的根文件系统。
  if (rv == -1) {
    exit(1);
  }

 /* Detach this process from its original group */
  rv = setsid();//退出原来的进程组,成为一个daemon。
  assert(rv > 0 && rv == getpid());

4.6 Child_Loop住循环:



int child_loop(wshd_t *w) {
  int sfd;
  int rv;

  close(STDIN_FILENO);
  close(STDOUT_FILENO);
  close(STDERR_FILENO);

  sfd = child_signalfd();//

  for (;;) {  //died loop
    fd_set fds;
    //FD_ZERO(fd_set *fdset);将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
    //FD_SET(int fd, fd_set *fdset);用于在文件描述符集合中增加一个新的文件描述符。
    //FD_ISSET(int fd, fd_set *fdset);用于测试指定的文件描述符是否在该集合中。
    FD_ZERO(&fds);
    FD_SET(w->fd, &fds);
    FD_SET(sfd, &fds);

    do {
      rv = select(FD_SETSIZE, &fds, NULL, NULL, NULL);//函数说明 select()用来等待文件描述词状态的改变,确认fd是否可读?
    } while (rv == -1 && errno == EINTR);

    if (rv == -1) {
      perror("select");
      abort();
    }

    if (FD_ISSET(w->fd, &fds)) {//FD_ISSET(int fd, fd_set *fdset);用于测试指定的文件描述符是否在该集合中。
      child_accept(w);
    }

    if (FD_ISSET(sfd, &fds)) {//SIGCHILD signal function
      struct signalfd_siginfo fdsi;

      rv = read(sfd, &fdsi, sizeof(fdsi));
      assert(rv == sizeof(fdsi));

      /* Ignore siginfo and loop waitpid to catch all children */
      child_handle_sigchld(w);
    }
  }

  return 1;
}
如果是套接字可读,那么就启动child_accept来接受请求;如果收到的是SIGCHILD信号,那么就启动child_handle_sigchild来处理,这个信号时容器销毁时候的信号,SIGCHILD(当子进程退出的时候会请求)

4.6.1  Child_accept

 

rv = accept(w->fd, NULL, NULL);接受链接请求

rv = un_recv_fds(fd, buf, buflen, NULL, 0);读取字符串
根据是否是交互式,分情况讨论,req.tty在wsh中设置会根据isatty来设置req.tty的值。
if (req.tty) {
    return child_handle_interactive(fd, w, &req);
  } else {
    return child_handle_noninteractive(fd, w, &req);
  }
4.6.1.1 child_handle_interactive:
首先创建一个pipe  
rv = pipe(p[1]);
打开一个pty,虚拟终端,把master保存在p[0][0]中,把slave保存在p[0][1]中
rv = openpty(&p[0][0], &p[0][1], NULL); openpty通过打开/dev/ptmx打开master,然后再dev/pts/*打开slave

然后把pipe以及虚拟终端的读端(master)传送给wsh进程。
rv = child_fork(req, p[0][1], p[0][1], p[0][1]);//int child_fork(msg_request_t *req, int in, int out, int err)
 assert(rv > 0);
然后child_fork出一个子进程去处理用户的请求
Child_fork:
将STDIN,STDOUT,STDERR都指向虚拟终端的写端。

rv = setsid();
将fork进程脱离原来的wshd进程,来执行wsh发送过来的进程。
envp = child_setup_environment(pw);
    assert(envp != NULL);
execvpe(argv[0], argv, envp);// envp contains environment variable
设置环境变量,然后通过execvpe把环境变量传入,然后执行用户传上来的脚本。


4.6.1.2 child_handle_noninteractive

首先创建四个pipe
  for (i = 0; i < 4; i++) {
    rv = pipe(p[i]);
    if (rv == -1) {
      perror("pipe");
      abort();
}
rv = un_send_fds(fd, (char *)&res, sizeof(res), p_, 4);//fd stands for warden.sock link
把管道0的写端,管道1,2,3的读端发送给wsh进程。
然后fork出一个子进程
rv = child_fork(req, p[0][0], p[1][1], p[2][1]);
将管道0的读端作为STDIN,将管道1,2的写端作为STDOUT,STDERR端

对于noninteractive来说child_pid_to_fd_add(w, rv, p[3][1]);把管道3的写端存入wshd_t这个结构体中。
对于interactive来说,
child_pid_to_fd_add(w, rv, p[1][1]);//将child_fork返回的pid和用于通信的管道(管道4)写端文件描述符记入wshd_t结构中。

4.6.2  child_handle_sigchld

处理子进程退出消息函数:
while (1) {
    do {
      pid = waitpid(-1, &status, WNOHANG); //等待子进程退出,并且取回子进程的执行状态。
    } while (pid == -1 && errno == EINTR);

    /* Break when there are no more children */
    if (pid <= 0) {
      break;
    }

    /* Processes can be reparented, so a pid may not map to an fd */
    fd = child_pid_to_fd_remove(w, pid);
    if (fd == -1) {
      continue;
    }

    if (WIFEXITED(status)) {//WIFEXITED(status)子进程正常退出则返回真,
      exitstatus = WEXITSTATUS(status);// WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值
      /* Send exit status to client */
      write(fd, &exitstatus, sizeof(exitstatus));//向wsh进程发送进程最后退出的状态
    } else {
      assert(WIFSIGNALED(status));// WIFSIGNALED(status)  若子进程异常终止则返回一个非零值。

      /* No exit status */
    }

    close(fd);
  }
}

如果子进程正常退出,那么向wsh进程发送最后退出的状态,否则,如果子进程真的异常退出(这判断是不是有点多余?)则assert。


WeiBo: ChampionLai
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐