warden wshd源码分析
wshd进程分析wshd是warden里面最重要的一个程序,它负责对发送给容器的指令的执行,在创建一个容器的时候会启动wshd这个进程。Main函数首先执行parent_run函数,parent_run里面的执行顺序如下: Hook-parent-before-clone.sh child_start函数 hook-parent-after-clon
·
wshd进程分析
* Hook-parent-before-clone.sh
* child_start函数
* hook-parent-after-clone.sh
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操作,不知道是否为这原因,文件系统还需进一步分析)
修改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中。
* 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命名空间。
pivot_root
hook_after_child_pivot.sh
exec(child_continue)
child_loop
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
execl("/sbin/wshd", "/sbin/wshd", "--continue", NULL);
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());
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(当子进程退出的时候会请求)
根据是否是交互式,分情况讨论,req.tty在wsh中设置会根据isatty来设置req.tty的值。
if (req.tty) {
return child_handle_interactive(fd, w, &req);
} else {
return child_handle_noninteractive(fd, w, &req);
}
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把环境变量传入,然后执行用户传上来的脚本。
首先创建四个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结构中。
处理子进程退出消息函数:
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
warden是cloudfoundry中比较核心的应用程序,负责应用程序的资源限制和隔离,而wshd是warden里面最重要的一个程序,它负责对发送给容器的指令的执行,在创建一个容器的时候会启动wshd这个进程。
首先上张图,wshd总体结构
* 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/wsh4.3 hook_after_child_pivot.sh:
准备虚拟终端虚拟终端需要的ptmx以及ptsmkdir -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_continueexecl("/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:
首先创建一个piperv = 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
更多推荐
已为社区贡献1条内容
所有评论(0)