Archive for March, 2016

如何给 kernel.org 下的项目贡献代码

March 17th, 2016

kernel的开发模式,不用细说了,直接切入正题!

1.  首先你总要把kernel-stable 和 linux-next git仓库克隆下来

2.  修改文件,blabla…. 然后使用 git 生成补丁

git format-patch -1

git 会将最近一次的提交生成补丁文件,可以在当前目录下看到 0001-*.patch 文件

3.  使用邮件发送你的 patch 给 maintainer 与 mailing list

代码库下会有文件描述 maintainer 的信息,这里以 rt-tests 为例,在 MAINTAINERS 文件中有 maintainer 的邮件地址,可将你的patch 发给上述地址。

4.  使用 Mutt 发送 patch
Mutt是一个命令行界面下的邮件客户端,具体配置方式这里就不再赘述,可以参考http://jingyan.baidu.com/article/0aa22375bbc3be88cc0d6425.html 来配置。

===================

如果遇到大patch的话,社区通常会reject,我们需要把patch分成若个小patch,每个patch叙述一部分功能,这样可以方便review,而且使得源码容易维护。那么如何拆分大patch呢?

比如这时在本地我们已经提交了一个大patch,然后可以执行以下操作:

1.  将当前提交撤销,重置到上一次。

$ git reset HEAD^

2.  通过补丁块拣选方式择要提交的修改。 Git 会逐一显示工作区更改,如果确认此处动要 会逐一显示工作区更改,如果确认此处动要提交,输入 “y“。

$ git add -p

以撤销提交的说明为蓝本,撰写新的commit。

$ git commit -e -C HEAD@{1}

3.  如果提交代码过于密集,耦合太强,那么上面这种方式不太适用,那么这时可以 直接编辑文件,删除要剥离出此次提交的修改然后执行:

$ git commit -- amend

然后执行下面的命令,还原有文件修改再提交:

$ git checkout HEAD@{1}-- .
$ git commit

 

参考:

http://jingyan.baidu.com/article/0aa22375bbc3be88cc0d6425.html

手动创建类docker环境

March 10th, 2016

最近docker特别的火,但是细究docker的原理机制。其实就是使用了cgroups+namespace完成资源的限制与隔离。现在我们手动创建一个namespace的容器,做到资源的隔离。

之前我们已经讨论了namespace的组成现在我们通过手动的方式创建每种不同namespace的环境。创建不同的namespace主要使用clone()+特定关键字的方式进行。我们可以把clone返回的pid,所以container也是一种特殊的进程!

Mount namespaces CLONE_NEWNS Linux 2.4.19
UTS namespaces CLONE_NEWUTS Linux 2.6.19
IPC namespaces CLONE_NEWIPC Linux 2.6.19
PID namespaces CLONE_NEWPID Linux 2.6.24
Network namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29
User namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8)

 

š 通过这个表格我们看到每个namespace完成时间不同,但是基于目前kernel版本已经为4.0.我们可以理解namespace部分基本完成。首先我们先定义一个模板:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];
char* const child_args[] = {
  "/bin/bash",
  NULL
};

int child_main(void* arg)
{
  printf(" - World !\n");
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  printf(" - Hello ?\n");
  int child_pid = clone(child_main, child_stack+STACK_SIZE, SIGCHLD, NULL);
  waitpid(child_pid, NULL, 0);
  return 0;
}

这里我们发现clone()的第二个参数是child_stack+STACK_SIZE,这里要说明下栈是从高地址往低地址走的,所以给出最后一个地址,也就是给出了栈的首地址。

1.UTS namespace[1]

使用这个UTS namespace可以使得容器拥有自己的主机名,程序是要是使用clone+sethostname()配合,这个与fork一个进程特别相似。

// (needs root privileges (or appropriate capabilities))
//[...]
int child_main(void* arg)
{
  printf(" - World !\n");
  sethostname("In Namespace", 12);
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  printf(" - Hello ?\n");
  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | SIGCHLD, NULL);
  waitpid(child_pid, NULL, 0);
  return 0;
}

与fork()函数类似,fork()函数创建一个新的child_pid时,进程中的内容与父进程完全相同,当后续执行子进程功能时,使用execv()族函数覆盖子进程各个程序段,包括代码段数据段等。这里我们注意到clone()函数中的CLONE_NEWUTS关键字,然后在子函数中调用execv(child_args[0], child_args);

注:child_args省略在运行程序添加参数

运行结果:

lzz@localhost:~/container$ gcc -Wall main.c -o ns && sudo ./ns
 - Hello ?
 - World !
root@In Namespace:~/container$ # inside the container
root@In Namespace:~/container$ exit
lzz@localhost:~/container$ # outside the container

上面的这个namespace只做到主机名的隔离,其他子系统都没有还没有隔离,我们在proc下还是可以看到全局的信息。

2.IPC namespace[2]

这里我们使用pipe进行同步,当创建child_pid时, checkpoint[0]为管道里的读取端,checkpoint[1]则为管道的写入端。当管道没有数据时,read()调用将默认的被阻塞,等待某些数据写入,从而达到同步的目的。

...
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);
  // wait...
  read(checkpoint[0], &c, 1);

  printf(" - World !\n");
  sethostname("In Namespace", 12);
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - Hello ?\n");

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);

  // some damn long init job
  sleep(4);
  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

这里在父进程下关闭close(checkpoint[1]);意味着父进程结束,子进程才能继续。

3. PID namespace[3]

PID namespace可以做到容器内部的pid与容器外的隔离,也就是说都可以有pid 1的进程,当然容器内pid 1 的进程映射到容器外,拥有其他的pid 号。

...
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);
  // wait...
  read(checkpoint[0], &c, 1);

  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - [%5d] Hello ?\n", getpid());

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | SIGCHLD, NULL);

  // further init here (nothing yet)

  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

这里我们看到在clone的标志里又加入了CLONE_NEWPID,然后在child_main中加入getpid(),我们可以发现容器的pid号。运行结果:

lzz@localhost:~/container$ gcc -Wall main-3-pid.c -o ns && sudo ./ns
 - [ 7823] Hello ?
 - [    1] World !
root@In Namespace:~/blog# echo "=> My PID: $$"
=> My PID: 1
root@In Namespace:~/blog# exit

这里我们发现在容器中,我们没有挂载proc文件系统,这里有一个问题,如果我们在容器里面挂载一个proc,在容器外使用top、ps -aux会提示出错。需要重新挂载proc在根目录下,因为这里我们并没有隔离文件系统。

4.CLONE_NEWNS[4]

这个clone选项,可以保证在容器内的文件挂载操作,不影响父容器的使用,也就解决了上面proc挂载损坏父容器空间的问题。

...
// sync primitive
int checkpoint[2];
....
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);

  // setup hostname
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);

  // remount "/proc" to get accurate "top" && "ps" output
  mount("proc", "/proc", "proc", 0, NULL);

  // wait...
  read(checkpoint[0], &c, 1);

  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - [%5d] Hello ?\n", getpid());

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);

  // further init here (nothing yet)

  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

这个时候我们运行这个程序:

lzz@localhost:~/container$ gcc -Wall ns.c -o ns && sudo ./ns
 - [14472] Hello ?
 - [    1] World !
root@In Namespace:~/blog# mount -t proc proc /proc
root@In Namespace:~/blog# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  1.0  0.0  23620  4680 pts/4    S    00:07   0:00 /bin/bash
root        79  0.0  0.0  18492  1328 pts/4    R+   00:07   0:00 ps aux
root@In Namespace:~/blog# exit

可以发现容器内的ps读取的是容器内部的proc文件系统!

这里我们就要考虑docker中,比如我们从docker hub下pull下一个镜像,在这个镜像中必然存在一个文件系统rootfs,包括各种配置信息,基本的链接库。各种特殊的文件系统:/dev,/sysfs等。这个就需要我们进行裁剪!
这里主要存在的目录有:

bin etc lib lib64 mnt proc run sbin sys tmp usr/bin 

我们需要根据自己的architecture配置这些目录。最后使用mount,将容器中的文件系统,都挂载到这些目录下。挂载完毕后使用chroot与chdir隔离目录:

    if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){
        perror("chdir/chroot");
    }

5.User namespace[5]

这个namespace主要是管理用户的UID,GID,主要原理通过读写/proc//uid_map 和 /proc//gid_map 这两个文件。这两个文件的格式为:ID-inside-ns ID-outside-ns length 。
核心函数:

void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}

void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

6.Network namespace[6]

这个namespace主要完成的是将一块物理网卡虚拟出多快虚拟网卡,主要命令:

# Create a "demo" namespace
ip netns add demo

# create a "veth" pair
ip link add veth0 type veth peer name veth1

# and move one to the namespace
ip link set veth1 netns demo

# configure the interfaces (up + IP)
ip netns exec demo ip link set lo up
ip netns exec demo ip link set veth1 up
ip netns exec demo ip addr add xxx.xxx.xxx.xxx/30 dev veth1
ip link set veth0 up
ip addr add xxx.xxx.xxx.xxx/30 dev veth0

运用在代码中就是:

...
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);

  // setup hostname
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);

  // remount "/proc" to get accurate "top" && "ps" output
  mount("proc", "/proc", "proc", 0, NULL);

  // wait for network setup in parent
  read(checkpoint[0], &c, 1);

  // setup network
  system("ip link set lo up");
  system("ip link set veth1 up");
  system("ip addr add 169.254.1.2/30 dev veth1");

  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - [%5d] Hello ?\n", getpid());

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD, NULL);

  // further init: create a veth pair
  char* cmd;
  asprintf(&cmd, "ip link set veth1 netns %d", child_pid);
  system("ip link add veth0 type veth peer name veth1");
  system(cmd);
  system("ip link set veth0 up");
  system("ip addr add 169.254.1.1/30 dev veth0");
  free(cmd);

  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

对于网络这一块,要想使得容器内的进程与容器外的进程通信,需要架设网桥,具体可以查看相关文档。

Final

我们这里看一下父子容器的namespace:
父:

lzz@localhost:~$ sudo ls -l /proc/4599/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026531838]

子:

lzz@localhost:~$ sudo ls -l /proc/4600/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026532520]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026532522]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026532521]

我们可以发现,其中的ipc,net,user是相同的ID,而mnt,pid,uts都是不同的。如果两个进程指向的namespace编号相同,就说明他们在同一个namespace下,否则则在不同namespace里面。

 

参考:

[1] http://lwn.net/Articles/531245/

[2] http://coolshell.cn/articles/17010.html

[3] http://lwn.net/Articles/532741/

[4] http://coolshell.cn/articles/17029.html

[5] http://lwn.net/Articles/539941/

[6] http://lwn.net/Articles/580893/

[7]

https://blog.jtlebi.fr/2014/01/12/introduction-to-linux-namespaces-part-4-ns-fs/
http://www.cnblogs.com/nufangrensheng/p/3579378.html