为 LXC 配置网络

October 16th, 2015 by JasonLe's Tech 2,156 views

LXC是一个基于cgroup 与 namespace 机制的轻量级虚拟机,在Ubuntu平台下有专门的源,可以直接通过apt-get安装,但是在debian平台下,软件仓库中lxc版本太低,导致很多新特性无法使用,推荐源码安装。截止到我写这篇博客,lxc版本已经更新至1.1.4 。

首先我们首先要编译安装最新版的LXC,根据教程INSTALL,我们需要运行autogen.sh ./configure 生成Makefile,这里必须将LXC 中的Security feature 全部安装,否则无法通过lxc-start 启动容器。

为容器配置网络有两种形式:1) 使用网桥    2) 直接使用物理网卡

1) 使用网桥

假设我们主机只有eth0的物理网卡,在主机/etc/network/interfaces中,直接加入下面的字段:

auto br0
iface br0 inet dhcp
        bridge_ports eth0
        bridge_fd 0
        bridge_maxwait 0

然后重启网络 /etc/init.d/networking restart 之后可以发现主机网络出现br0的网桥。

如果LXC在编译时没有配置路径,容器的config默认路径在/usr/local/var/lib/lxc/xxx/config ,我们需要在这个文件中加入网络选项

lxc.network.type = veth
lxc.network.flags = up

# that's the interface defined above in host's interfaces file
lxc.network.link = br0

# name of network device inside the container,
# defaults to eth0, you could choose a name freely
# lxc.network.name = lxcnet0 

lxc.network.hwaddr = 00:FF:AA:00:00:01

然后我们在容器的/etc/network/interfaces中,添加

auto eth0
iface eth0 inet dhcp

如果容器中没有开启dhclient服务,最好将其加到 /etc/rc.local中即可。

2) 直接使用物理网卡

比如物理宿主主机拥有两张网卡:eth0 与 eth1,我把eth0作为主机使用,eth1作为LXC使用。那么我们在config中添加

xc.network.type=phys
lxc.network.link=eth1
lxc.network.flags=up
#lxc.network.hwaddr = 00:16:3e:f9:ad:be #注释掉#

lxc.network.flags 用于指定网络的状态,up 表示网络处于可用状态。
lxc.network.link 用于指定用于和容器接口通信的真实接口,比如一个网桥 br0 ,eth0等。

在主机/etc/network/interfaces中加入

auto eth1
iface eth1 inet dhcp

然后重新启动网络服务 #/etc/init.d/networking restart
重新启动 LXC 容器 # lxc-start -n xxx

一旦 LXC 虚拟计算机启动成功,在宿主计算机上使用〝ifconfig -a〞查看主机网络接口,用户会发现此时网络接口 eth1 消失了,只有 eth0 。这是因为 eth1 已经让 LXC 虚拟计算机给使用了。然后我们使用如下命令“ lxc-attach -n xxx”登录 LXC 虚拟计算机发现此时 LXC 虚拟计算机的网络接口是 eth1。然后我们可以使用 ping 命令测试一下 LXC 虚拟计算机和互联网是否联通。

3) 容器配置静态IP

如果我们使用静态IP的话,宿主机可以使用静态IP或者是DHCP,我们假定宿主机是DHCP,容器是静态IP,注意最后两个字段:

lxc.network.type = veth
lxc.network.flags = up

# that's the interface defined above in host's interfaces file
lxc.network.link = br0

# name of network device inside the container,
# defaults to eth0, you could choose a name freely
# lxc.network.name = lxcnet0 

lxc.network.hwaddr = 00:FF:AA:00:00:01
lxc.network.ipv4 = 192.168.1.110/24#注意设置为宿主机的网段
lxc.network.ipv4.gateway = 192.168.1.1#注意设置为宿主机的网段

在容器内的/etc/network/interfaces中加入,记住不加auto eth0!

iface eth0 inet static
       address <container IP here, e.g. 192.168.1.110>
       netmask 255.255.255.0
       network <network IP here, e.g. 192.168.1.0>
       broadcast <broadcast IP here, e.g. 192.168.1.255>
       gateway <gateway IP address here, e.g. 192.168.1.1>
       # dns-* options are implemented by the resolvconf package, if installed
       dns-nameservers <name server IP address here, e.g. 192.168.1.1>
       dns-search your.search.domain.here

结束:

根据我与CRIU团队的交流,目前CRIU不支持对于LXC独占物理网卡的c/r ,对于某些application使用 SOCK_PACKET 的套接字目前也不支持!这个特性已被加到criu新特性中,https://github.com/xemul/criu/issues/73 。预计在之后的版本中支持!

 

https://www.ibm.com/developerworks/cn/linux/1312_caojh_linuxlxc/

https://wiki.debian.org/LXC/SimpleBridge

使用kernel-package编译内核img包

October 11th, 2015 by JasonLe's Tech 1,484 views

之前编译内核一般也使用make –> make modules_install –> make install –> update-grub 这一系列步骤,在Debian、Ubuntu机器上可以使用kernel-package来编译安装内核。kernel-package是Debian提供的一个编译Linux内核的一个工具集,安装kernel-package 会同时安装上build-essential、libncurses-dev、linux-source等一系列工具。

首先安装:# apt-get install kernel-package

安装完成后我们可以使用dpkg 查看一下:# dpkg -l

在打印出来的信息中我们可以看到,kernel-package 是 A utility for building Linux kernel related 也就是一个用来构建内核的工具。

$dpkg -l kernel-package
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                Version        Architecture   Description
+++-===================-==============-==============-===========================================
ii  kernel-package      12.036+nmu3    all            A utility for building Linux kernel related

我们会发现安装完kernel-package之后make等一系列工具也安装了,然后我们还是编译一个内核看一下kernel-package的作用:

# cd  linux-3.18.21    // 进入你想要编译的内核的解压文件夹
# make menuconfig   // 编译内核,自己选择
# sudo CONCURRENCY_LEVEL=4 make-kpkg --initrd kernel-image  // 这一句就是在使用kernel-package在编译。

CONCURRENCY_LEVEL=4 是设置多线程(类似于我们make -j4的多线程控制), make-kpkg就是kernel-package提供的编译工具,–initrd参数是说明在生成的image包里有initrd

……

dpkg --build      /home//kernel/linux-3.18.21/debian/linux-image-3.18.21
dpkg-deb: building package `linux-image-3.18.21' in `../linux-image-linux-3.18.21.Custom_i386.deb'.
make[2]: Leaving directory `/home/kernel/linux-3.18.21'
make[1]: Leaving directory `/home/kernel/linux-3.18.21'

从上面的信息可以看出,我们的make-kpkg生成了一个deb文件,其实这就是我们编译好的内核,放在当前内核文件夹的上层目录。到此我们的编译工作结束,我们可以使用dpkg 安装我们刚编译好的目录!如果你的编译的内核对其他机器也适用,你可以拷贝这个deb文件到其他机器上直接安装使用。这个工具使得我们编译内核工作变得更加简单快捷。使用kernel-package编译内核最大的好处是我们可以使用  dpkg -r 删除我们编译的内核。

 

安装我们使用的是 sudo dpkg -i linux-image-3.18.21.Custom_i386.deb
卸载时我们可以直接使用: sudo dpkg -r  linux-image-3.18.21

 

 

 

内核线程中poll的操作

October 8th, 2015 by JasonLe's Tech 1,624 views

在用户空间我们可以使用poll()函数或者select()函数对一个设备进行轮训操作,但是在内核空间呢?虽然read()/write()在内核空间有vfs统一管理,故我们可以使用vfs_read()/vfs_write()对文件进行读取(参见)。但是我找不到vfs_poll()。要想实现poll的功能,考虑使用等待队列造个poll的轮子

如果我们设计一个字符设备,这个字符设备出现数据的时候,我们需要在适当的wake_up(),在创建内核线程中,我们需要实现一个业务逻辑:

DECLARE_WAIT_QUEUE_HEAD(my_waitqueue);
static int xxx_kernel_thread(void)
{
       DECLARE_WAITQUEUE(wait,current);
       while(1)
       {
              add_wait_queue(&my_waitqueue,&wait);
              set_current_state(TASK_INTERRUPTIBLE);

              schedule();
              set_current_state(TASK_RUNNING);
              remove_wait_queue(&my_waitqueue,&wait);
              //do_something
        }
...
}

这个唤醒操作很有意思,当我们将当前等待项加入到等待队列后,设置当前的内核线程睡眠,主动调用schedule()让出cpu,当其他的某个地方唤醒这个等待队列后,代码从schedule()下一句开始执行。然后将当前内核线程设置为运行,然后移除等待项,通过这种业务逻辑做到了内核线程的轮询。

我在编写这个代码的时候犯了一个低级错误内核线程被唤醒后,没有设置TASK_RUNNING,而直接移除等待队列,这个就会导致BUGON的产生,虽然业务逻辑可以顺利执行,有时间需要看看调度的流程,才可以透彻的理解调度的实际含义。

 

http://dashan8020.blog.163.com/blog/static/4796750420115180227132/

在内核中对文件进行读写

September 28th, 2015 by JasonLe's Tech 2,289 views

我们知道在用户态下,使用各种文件的系统调用即可对文件进行读写操作,open()、write()、read()等。这些调用最后都会通过内核的VFS模型,调用到设备驱动函数,这里我们可以简单看一下open()、write()、read()驱动函数接口:

static int xxx_open(struct inode *inode, struct file *file);
static int xxx_release(struct inode *inode, struct file *file);
static ssize_t xxx_read(struct file *filp, char __user *ubuf,
                                  size_t usize, loff_t *off);
static unsigned int xxx_poll(struct file *file, poll_table *wait);
static long xxx_ioctl(struct file *f, unsigned int cmd,
                                  unsigned long arg);
ssize_t xxx_write(struct file *filp, const char __user *ubuf,
                          size_t usize, loff_t *off)

实际上在内核驱动调用的函数传入的参数远比我们在用户态下看到的复杂的多,那么问题来了:如果在内核态下对文件进行读取?当然我们不能在内核中使用syscall了,这里我们有两种方式:1.将全部的驱动函数导出 2.使用VFS在内核态中的接口。

将全部的驱动函数导出的方式虽然可行,但是这样做等于将函数暴露在全局,不利于封装,不推荐使用这种方式。第二种是我们推荐的方式,内核为开发者提供了filp_open(),filp_close(),vfs_read(),vfs_write(),vfs_fsync()接口,我们只需要调用这些接口即可在内核态下对文件进行操作。

首先我们要包含头文件:

#include <linux/fs.h>
#include <asm/segment.h>
#include <asm/uaccess.h>
#include <linux/buffer_head.h>

当然了,很多时候我们在内核层下封装了这些接口,使其可以像用户态下open那般简单易用。不过我们要注意open的返回值不再是fd,而是struct file *类型的指针!而path就是路径,flag是读写权限。

struct file* file_open(const char* path, int flags, int rights) {
    struct file* filp = NULL;
    mm_segment_t oldfs;
    int err = 0;

    oldfs = get_fs();
    set_fs(get_ds());
    filp = filp_open(path, flags, rights);
    set_fs(oldfs);
    if(IS_ERR(filp)) {
        err = PTR_ERR(filp);
        return NULL;
    }
    return filp;
}

关闭一个文件:

void file_close(struct file* file) {
    filp_close(file, NULL);
}

读取文件的封装接口参数比较多,第一个参数是文件指针,第二个是偏移量,第三个是buffer,第四个是读取的大小。与用户态下的read()类似!

int file_read(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_read(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}   

写数据到文件中:

int file_write(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_write(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}

立即回写到磁盘,同步文件:

int file_sync(struct file* file) {
    vfs_fsync(file, 0);
    return 0;
}

 

http://stackoverflow.com/questions/1184274/how-to-read-write-files-within-a-linux-kernel-module
https://en.wikipedia.org/wiki/Virtual_file_system

字符驱动poll函数与select()函数的交互

September 25th, 2015 by JasonLe's Tech 2,055 views

在字符驱动中,我们经常要实现poll()的功能,具体实现在注册到file_operations 的函数中,举个例子

static unsigned int xxx_poll(struct file *file, poll_table *wait)
{
         poll_wait(file, &mp_chrdev_wait, wait);
         if (rcu_access_index(mplog.next))
                 return POLLIN | POLLRDNORM;
 
         return 0;
}

我们必须在这个函数中返回POLLIN、POLLOUT等状态,从而我们可以在用户态下使用FD_ISSET()判断数据是否到来。而其中void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);它的作用就是把当前进程添加到wait参数指定的等待列表(poll_table)中。需要注意的是这个函数是不会引起阻塞的。

这里我们实现创建了一个mp_chrdev_wait的等待队列,它会把这个轮训进程放入一个等待队列中,然后这个进程会睡眠(表现在select()上就是阻塞)。当某个条件满足时,唤醒这个等待队列,也就是唤醒了轮训进程,也就是内核通知应用程序(应用程序的select函数会感知),这个时候mask返回值中有数据。然后就会接着select操作。所以我们要在恰当的位置wake_up_interruptible(&mp_chrdev_wait)

在用户空间中的代码,我们需要使用select()轮训在这个设备上:

          int register_fd, ret;
          fd_set rds;
  
          register_fd = open(CONFIG_PATH, O_RDWR);
          if (register_fd < 0)
                 err("opening of /dev/mplog");
 
          FD_ZERO(&rds);
          FD_SET(register_fd,&rds);
  
  
          while (1) {
                  /*
                   * Proceed with the rest of the daemon.
                  */
                 memset(temp, 0, MP_LOG_LEN * sizeof(struct mp));
  
 
                  ret = select(register_fd+1,&rds,NULL,NULL,NULL);
                  if(ret < 0 )
                  {
                          close(register_fd);
                          err("select error!");
                  }
                  if(FD_ISSET(register_fd,&rds))
                         read(register_fd, temp, MP_LOG_LEN * sizeof(struct mp));
 .... 
          }