Python中的正则表达式

基于web技术的操作系统安装器的设计

什么是基于web技术的安装器(web-based installer)?

传统的Linux操作系统安装需要启动一个LiveOS,然后在LiveOS中运行一个本地安装程序,如Fedora下的Anaconda. LiveOS除了让用户在安装操作系统之前能预先体验之外,也为安装器提供了运行环境。这对于桌面操作系统已然足够,因为PC、笔记本电脑自带终端设备——键盘、显示器、鼠标。然而,如果要给一台服务器安装操作系统则稍微复杂了一点,因为服务器通常没有这些终端设备。这就需要利用网络和VNC将服务器端的图像传送出来。这样做的缺点在于,网络负载很大,在网络条件不好的环境下会带给用户非常差的安装体验。另外,本地安装器也会依赖很多的图形软件包,不仅会增加ISO的大小,也会带来很多版权及法律上的工作量。

基于web技术的安装器则利用web开发技术——HTML5,Javascript,CSS,web server等,让用户可以通过浏览器直接安装操作系统。目前,”KVM for IBM z Systems”已经采用这种安装方式。

基于web技术安装器的优势

  • 依赖更少的软件包
  • 非常小的网络负载,网页加载后只需要通过Ajax来与服务器通信
  • 可通过浏览器跨平台访问
  • 对于服务器及集群安装非常方便
  • 易于与其他软件集成,提供RESTful API
  • 满足可访问性,方便残障人士使用

使用web安装器的安装流程

使用web安装器来安装操作系统的流程非常简单,只需要如下几个步骤:

  1. 加载ISO
  2. 用https://{IP_OF_SERVER}作为URL来访问安装器
  3. 按照安装器的引导完成安装
  4. 点击安装器的重启按钮重启服务器,安装完成

Web安装器实现的功能

  • 国际化及多语言支持,选择安装语言与系统语言
  • 版权声明
  • 磁盘列表及选择安装磁盘
  • 添加SCSI磁盘
  • 添加DASD磁盘
  • Swap分区加密
  • 自动分区
  • 手动分区
  • 分区操作列表
  • 激活网卡
  • IPv4配置
  • IPv6配置
  • 主机名及搜索域配置
  • NTP配置
  • Kdump配置
  • 时区设置
  • Root密码设置
  • 添加、删除用户
  • 配置总结列表
  • 无人值守安装
  • 安装日志下载

Web安装器的架构设计

Web安装器分为前端和后端两部分实现:前端负责UI展示及用户交互,后端负责给前端提供RESTful API并根据前端的API调用来存储用户配置数据,执行分区、安装、配置目标系统等操作。

Web安装器由4个HTML页面组成:

  • 欢迎页:介绍操作系统,提供选择安装语言的下拉框,点击下一步可进入到版权声明页
  • 版权声明页:显示版权文件,在用户同意后可跳转到配置页面
  • 配置页:引导用户进行系统配置
  • 安装页:展示安装进度,安装完成后可点击重启按钮重启系统

UI是基于HTML5、CSS3及Javascript等网页开发技术,并利用如下工具:

  • jQuery:一个快速、小巧且功能丰富的js库,可用来操作DOM,处理事件及Ajax请求
  • Bootstrap:最流行的前端开发框架之一,多用于开发响应式、移动优先的web项目
  • Bootstrap-select: jQuery 插件,利用Bootstrap,但提供了功能更加丰富的下拉选择框控件
  • Jquery Validation :jQuery插件,用来检验表单的合法性

后端由以下几个模块组成:

  • CherryPy:一个轻量级的python web发布器
  • Model:存储用户的配置数据
  • RESTful API:为前端提供API接口
  • 子功能模块:提供安装器的各个子功能

QEMU3 – 使用ceph来存储QEMU镜像

ceph简介

Ceph是一个PB级别的分布式软件定义存储系统,为用户提供了块存储、对象存储以及符合POSIX标准的文件系统接口。目前,Ceph已经成为Openstack最受欢迎的后端存储系统。下图为ceph的架构图。

ceph架构图

RADOS本身是一个对象存储系统,实现了ceph的核心功能。Librados是ceph提供给各种编程语言的接口。RADOSGW,RBD,CEPH FS分别为用户提供了对象存储、块存储及文件系统的功能。Ceph集群及客户端的安装配置请参考Ceph官方文档

使用Ceph来存储QEMU镜像

QEMU会假定ceph配置文件存放在默认位置/etc/ceph/$cluster.conf,也会使用client.admin作为默认的ceph用户。如果要指定其他的配置文件或者用户,可以在ceph RBD的选项中添加conf=/home/ceph.conf或者id=admin选项。qemu-img使用ceph块存储RBD时,需要使用下面的格式:

qemu-img {command} [options] rbd:{pool-name}/{image-name}[@snapshot-name][:option1=value1][:option2=value2…] 

例如:

qemu-img {command} [options] rbd:glance-pool/maipo:id=glance:conf=/etc/ceph/ceph.conf 

创建一个镜像

可以使用qemu-img命令在ceph集群中创建一个虚拟机镜像。需要指定rbd, pool,以及镜像名。

qemu-img create -f raw rbd:{pool-name}/{image-name} {size} 

例如:

[root@ltczhp20 ~]# qemu-img create -f raw rbd:rbd/vmdisk1 4G

Formatting 'rbd:rbd/vmdisk1', fmt=raw size=4294967296
[root@ltczhp20 ~]# rbd ls
vmdisk1

qemu-img通常会指定RBD存储的镜像格式是RAW,这样可以减少其他格式带来的性能开销,也会防止虚拟机热迁移时缓存带来的问题。

调整镜像的大小

要调整镜像大小,必须指定rbd,pool name,以及镜像名。

qemu-img resize rbd:{pool-name}/{image-name} {size} 

例如:

[root@ltczhp20 ~]# qemu-img resize -f raw rbd:rbd/vmdisk1 2G

Image resized.
[root@ltczhp20 ~]# rbd ls
vmdisk1
[root@ltczhp20 ~]# rbd info vmdisk1
rbd image 'vmdisk1':
    size 2048 MB in 512 objects
    order 22 (4096 kB objects)
    block_name_prefix: rbd_data.fa802ae8944a
    format: 2
    features: layering, exclusive-lock, object-map, fast-diff, deep-flatten
    flags:

如果不指定镜像格式(-f raw),qemu会给出警告信息:

[root@ltczhp20 ~]# qemu-img resize rbd:rbd/vmdisk1 4G
WARNING: Image format was not specified for 'rbd:rbd/vmdisk1' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
Image resized.

获取镜像信息

获取镜像信息同样需要指定rbd,pool name以及镜像名:

qemu-img info rbd:{pool-name}/{image-name} 

例如:

[root@ltczhp20 ~]# qemu-img info rbd:rbd/vmdisk1

image: rbd:rbd/vmdisk1
file format: raw
virtual size: 4.0G (4294967296 bytes)
disk size: unavailable
cluster_size: 4194304

使用qemu命令运行虚拟机

从QEMU0.15后,虚拟机使用ceph块设备就不需要使用rbd map命令将RBD镜像映射到本地了,QEMU可以通过librados直接访问一个虚拟块设备。这样避免了额外的上下文切换,也充分利用了RBD的缓存功能。

在运行虚拟机之前,我们可以把一个已经存在的虚拟机镜像转化为ceph RBD存储,然后直接从RBD启动虚拟机。

qemu-img convert -c -f fmt -O out_fmt -o options  fname out_fname 

例如:

[root@ltczhp20 ~]# qemu-img convert -f qcow2 -O raw /srv/fedora24/fedora24.qcow2 rbd:rbd/fedora

然后使用qemu命令运行虚拟机。

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora

RBD缓存会极大的提高虚拟机的性能。QEMU1.2之后,cache选项可以直接控制librbd:

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora,cache=writeback

在QEMU1.2之前,如果要使用RBD缓存,需要额外添加rbd_cache=true选项:

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora,cache=writeback,rbd_cache=true

如果指定了rbd_cache=true,一定要指定cache=writeback,否则QEMU不会给librbd发送flush请求,RBD之上的文件系统可能会被破坏。

使用ceph RBD的快照功能

创建一个镜像快照sp0:

[root@ltczhp20 ~]# qemu-img snapshot -l rbd:rbd/fedora
[root@ltczhp20 ~]# qemu-img snapshot -c sp0 rbd:rbd/fedora
WARNING: Image format was not specified for 'rbd:rbd/fedora' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
[root@ltczhp20 ~]# qemu-img snapshot -l rbd:rbd/fedora
Snapshot list:
ID        TAG                 VM SIZE                DATE       VM CLOCK
sp0       sp0                     20G 1970-01-01 01:00:00   00:00:00.000

启动虚拟机,创建文件/root/hello.txt并写入字符串”hello world”,然后关闭虚拟机。

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora

In VM:

[root@localhost ~]# echo "hello world" >> /root/hello.txt
[root@localhost ~]# cat /root/hello.txt
hello world

[root@localhost ~]# halt

将虚拟机回滚到快照sp0,然后检查是否存在/root/hello.txt文件,如果不存在则说明快照已经成功回滚。

[root@ltczhp20 ~]# qemu-img snapshot -a sp0 rbd:rbd/fedora

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora

In VM:

[root@localhost ~]# ls /root/hello.txt
ls: cannot access '/root/hello.txt': No such file or directory

[root@localhost ~]# halt

删除快照:

[root@ltczhp20 ~]# rbd snap ls rbd/fedora
SNAPID NAME     SIZE
    22 sp0  20480 MB
[root@ltczhp20 ~]# qemu-img snapshot -d sp0 rbd:rbd/fedora
WARNING: Image format was not specified for 'rbd:rbd/fedora' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
[root@ltczhp20 ~]# rbd snap ls rbd/fedora
[root@ltczhp20 ~]# qemu-img snapshot -l rbd:rbd/fedora

阅读原文

顺序存储线性表的实现

最近复习数据结构,写了一个顺序存储的线性表,代码粘在这里:)

代码下载:git@github.com:Wang-Sen/algorithm.git

/*
 * Simple array implementation.
 */

#include <stdbool.h>

#define MAX_SIZE 50
#define data_t int
#define array_length_t int

#define ERR_OUT_OF_RANGE -1
#define ERR_EMPTY_ARRAY -2
#define ERR_INVALID_ARGS -3

typedef struct {
    data_t data[MAX_SIZE];
    array_length_t length;
} array;

static inline void array_init(array *arr)
{
    for (int i = 0; i < MAX_SIZE; i++) {
        arr->data[i] = 0;
    }
    arr->length = 0;
}

static inline void show_array(array *arr)
{
    for (int i=0; i < arr->length; i++) {
        printf("%d ", arr->data[i]);
    }
    printf("\n");
}

static inline bool is_array_empty(array *arr)
{
    if (!arr) {
        return ERR_INVALID_ARGS;
    }
    return !arr->length;
}

static inline array_length_t len(array *arr) {
    if (!arr) {
        return ERR_INVALID_ARGS;
    }
    return arr->length;
}

static inline int locate_entry(array *arr, data_t entry) {
    if (!arr) {
        return ERR_INVALID_ARGS;
    }
    int ret = -1;
    for (int i = 0; i < MAX_SIZE; i++) {
        if (arr->data[i] == entry) {
            ret = i;
            return ret;
        }
    }
    return ret;
}

static inline int array_push(array *arr, data_t entry)
{
    if (!arr) {
        return ERR_INVALID_ARGS;
    }
    if ((arr->length + 1) > MAX_SIZE) {
        return ERR_OUT_OF_RANGE;
    }
    arr->data[arr->length] = entry;
    (arr->length)++;
    return 0;
}

static inline int array_pop(array *arr, data_t *p_entry)
{
    if (arr->length <= 0) {
        return ERR_EMPTY_ARRAY;
    }
    if (!p_entry || !arr) {
        return ERR_INVALID_ARGS;
    }

    *p_entry = arr->data[arr->length - 1];
    (arr->length)--;

    return 0;
}

static inline int array_insert(array *arr, int loc, data_t entry)
{
    if (!arr) {
        return ERR_INVALID_ARGS;
    }
    if ((arr->length + 1) > MAX_SIZE || loc < 0 || loc >= MAX_SIZE) {
        return ERR_OUT_OF_RANGE;
    }
    for (int i = arr->length; i > loc; i--) {
        arr->data[i] = arr->data[i - 1];
    }
    arr->data[loc] = entry;
    (arr->length)++;
    return 0;
}

static inline int array_delete(array *arr, int loc, data_t *entry)
{
    if (!arr || !entry) {
        return ERR_INVALID_ARGS;
    }
    if (loc < 0 || loc >= arr->length) {
        return ERR_OUT_OF_RANGE;
    }

    *entry = arr->data[loc];
    for (int i = loc; i < (arr->length) - 1; i++) {
        arr->data[i] = arr->data[i + 1];
    }
    (arr->length)--;
    return 0;
}

static inline int array_update(array *arr, int loc, data_t entry)
{
    if (!arr) {
        return ERR_INVALID_ARGS;
    }
    if (!arr->length) {
        return ERR_EMPTY_ARRAY;
    }
    if (loc < 0 || loc >= arr->length) {
        return ERR_OUT_OF_RANGE;
    }
    arr->data[loc] = entry;
    return 0;
}

static inline int array_reverse(array *arr)
{
    if (!arr) {
        return ERR_INVALID_ARGS;
    }

    int i, j, tmp;
    for (i = 0, j = (arr->length - 1); j - i > 0; i++, j--) {
        if (arr->data[i] == arr->data[j]) {
            tmp = arr->data[i];
            arr->data[i] = arr->data[j];
            arr->data[j] = tmp;
        } else {
            arr->data[i] ^= arr->data[j];
            arr->data[j] ^= arr->data[i];
            arr->data[i] ^= arr->data[j];
        }
    }

    return 0;
}

测试代码:

#include <stdio.h>
#include <unistd.h>
#include "array.h"

int main(int argc, char **argv) {
    int ret = 0, i;
    array list;
    array_init(&list);

    /* test is_empty_array */
    printf("Empty array: %d\n", is_array_empty(&list));
    list.data[0] = 2400;
    list.length += 1;
    printf("Empty array: %d\n", is_array_empty(&list));
    /* end test is_empty_array */

    /* test push */
    for (i = list.length; i < MAX_SIZE; i++) {
        if ((ret = array_push(&list, i))) {
            printf("Err: %d\n", ret);
            printf("length: %d\n", list.length);
            printf("Entry: %d\n", i);
        }
    }

    show_array(&list);

    if ((ret = array_push(&list, 20))) {
        printf("Err: %d\n", ret);
        printf("length: %d\n", list.length);
        printf("Entry: %d\n", 20);
    }
    /* end test push */

    /* test insert */
    array_init(&list);
    for (i = list.length; i < MAX_SIZE-1; i++) {
        if ((ret = array_push(&list, i))) {
            printf("Err: %d\n", ret);
            printf("length: %d\n", list.length);
            printf("Entry: %d\n", i);
        }
    }
    array_insert(&list, 3, 5);
    show_array(&list);

    array_insert(&list, 3, 6);
    show_array(&list);

    array_init(&list);
    for (i = list.length; i < MAX_SIZE-1; i++) {
        if ((ret = array_push(&list, i))) {
            printf("Err: %d\n", ret);
            printf("length: %d\n", list.length);
            printf("Entry: %d\n", i);
        }
    }
    array_insert(&list, MAX_SIZE, 5);
    show_array(&list);
    /* end test insert */

    /* test locate_entry */
    array_init(&list);
    for (i = list.length; i < MAX_SIZE; i++) {
        if ((ret = array_push(&list, i))) {
            printf("Err: %d\n", ret);
            printf("length: %d\n", list.length);
            printf("Entry: %d\n", i);
        }
    }
    printf("location of 5 is: %d\n", locate_entry(&list, 5));
    printf("location of 10 is: %d\n", locate_entry(&list, 10));
    printf("location of 100 is: %d\n", locate_entry(&list, 100));
    printf("location of -100 is: %d\n", locate_entry(&list, -100));
    /* end test locate_entry */

    /* test len*/
    array_init(&list);
    for (i = 0; i < 38; i++) {
        if ((ret = array_push(&list, i))) {
            printf("Err: %d\n", ret);
            printf("length: %d\n", list.length);
            printf("Entry: %d\n", i);
        }
    }
    printf("len : %d\n", len(&list));
    /* end test len */

    /* test pop */
    array_init(&list);
    int j = 0;
    for (i = 0; i < 26; i++) {
        array_push(&list, j);
        j += 2;
    }
    show_array(&list);
    for (i = 0; i < 27; i++) {
        array_pop(&list, &j);
        printf("Pop Entry: %d\n", j);
        show_array(&list);
    }
    /* end test pop */

    /* test delete*/
    array_init(&list);
    for (i = 0; i < 26; i++) {
        array_push(&list, j);
        j += 2;
    }
    show_array(&list);
    for (i = 0; i < 27; i++) {
        if (array_delete(&list, i, &j) != 0) {
            printf("Delete %dth entry error!\n", i);
            break;
        }
        printf("Deleted Entry: %d\n", j);
        show_array(&list);
    }
    /* end test delete */

    /* test update*/
    array_init(&list);
    for (i = 0, j = 0; i < 26; i++) {
        array_push(&list, j);
        j += 2;
    }
    show_array(&list);
    for (i = 0; i < 28; i++) {
        if (array_update(&list, i, 99) != 0) {
            printf("Update %dth entry error!\n", i);
            break;
        }
        printf("Updated %dth Entry: %d\n", i, j);
        show_array(&list);
    }
    /* end test update */

    /* test reverse*/
    array_init(&list);
    for (i = 0; i < MAX_SIZE; i++) {
        array_push(&list, i);
    }
    show_array(&list);
    array_reverse(&list);
    show_array(&list);
    /* end test reverse */
}

在fedora下使用搜狗拼音输入法

Linux下的拼音输入法实在是不敢恭维,还好有人把搜狗拼音输入法制作成了RPM包.安装此rpm包就可以在Linux下面使用搜狗拼音输入法及其字库了.

第一步,下载RPM包.

百度网盘地址:http://pan.baidu.com/s/1bpblFoN

第二步,安装RPM包.

    $sudo yum install  fcitx-sogoupinyin-0.0.4-1.fc20.x86_64.rpm  //注意输入正确的路径

第三步,卸载ibus.

    $sudo yum remove ibus

第四步,设置fcitx开机自动启动.

    $sudo yum install gnome-tweak-tool

    $gnome-tweak-tool  //在开机启动一项中添加fcitx即可

第五步,重启gnome

    $gnome-session-quit

最后,使用Ctrl+space愉快的玩耍.

阅读原文

使用jQuery Validation插件来验证表单

jQuery Validation是一个用于验证表单的jQuery插件,简单易用,已经包含了16种内置的验证规则.Github上也有更多的验证规则可以使用.这都不是重点,重点是你可以轻松的定制自己的规则.

内置规则:

  • required – Makes the element required.
  • remote – Requests a resource to check the element for validity.
  • minlength – Makes the element require a given minimum length.
  • maxlength – Makes the element require a given maximum length.
  • rangelength – Makes the element require a given value range.
  • min – Makes the element require a given minimum.
  • max – Makes the element require a given maximum.
  • range – Makes the element require a given value range.
  • step – Makes the element require a given step.
  • email – Makes the element require a valid email
  • url – Makes the element require a valid url
  • date – Makes the element require a date.
  • dateISO – Makes the element require an ISO date.
  • number – Makes the element require a decimal number.
  • digits – Makes the element require digits only.
  • equalTo – Requires the element to be the same as another one

内置规则的使用

内置规则的使用非常简单:

首先将该插件的js文件包含进html文件:

    <script src="/static/js/jquery.min.js"></script>
    <script src="/static/js/jquery.validate.min.js"></script>

然后用jQuery选择需要验证的表单,执行validate()函数即可:

    <script>
    $("#form_id").validate();
    </script>

jQuery Validation官网上的例子:

    <!DOCTYPE HTML>
    <html>
    <head>
    <script src="jquery.min.js"></script>
    </head>
    <body>
    <form class="cmxform" id="commentForm" method="get" action="">
    <fieldset>
    <legend>Please provide your name, email address (won't be published) and a comment</legend>
    <p>
    <label for="cname">Name (required, at least 2 characters)</label>
    <input id="cname" name="name" minlength="2" type="text" required>
    </p>
    <p>
    <label for="cemail">E-Mail (required)</label>
    <input id="cemail" type="email" name="email" required>
    </p>
    <p>
    <label for="curl">URL (optional)</label>
    <input id="curl" type="url" name="url">
    </p>
    <p>
    <label for="ccomment">Your comment (required)</label>
    <textarea id="ccomment" name="comment" required></textarea>
    </p>
    <p>
    <input class="submit" type="submit" value="Submit">
    </p>
    </fieldset>
    </form>
    <script>
    $("#commentForm").validate();
    </script>
    </body>
    </html>

jQuery Validation会根据表单设置的type和属性自动为他们分配内置的规则,比如email,url,required等.

运行一下看看:

什么都不输入,直接点提交:

空表单

输入错误的Email地址,改正后错误提示自动消失: Email输入错误Email输入正确

添加自定义规则

jQuery Validation最吸引人的feature,它可以轻松的加入自定义的规则:

第一步,在js中调用jQuery.validator.addMethod函数来添加规则,例如添加IP格式检查的规则:

$.validator.addMethod( "ipv4", function( value, element ) {return this.optional( element ) || /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i.test( value );}, "Invalid IP v4 address." ); //自定义其他规则只需要替换规则名"ipv4",正则表达式//之间的内容,以及出错后显示的字符串"Invalid IP v4 address."即可.

第二步,把规则应用到指定的表单项,即在执行$(“#form_id”).validate()函数的时候加入rules参数:

    23 $("#ip_form").validate({
    24     rules: {
    25         ip: {
    26             required: true,
    27             ipv4: true,
    28         },
    29         netmask: {
    30             required: true,
    31             ipv4: true,
    32         },
    33         gateway: {
    34             required: true,
    35             ipv4: true,
    36         },
    37     },
    38 }); //其中ip,netmask,gateway为表单项的name属性.required和ipv4是规则名.

生效后的样子,可以添加如下css来修改错误信息的样式:

    label.error {
        margin-left: 10px;
        padding-left: 5px;
        padding-right: 5px;
        color: #E2303A;
        font-style: italic;
        font: Helvetica Neue, 12px, #E2303A;
        border: 1px solid #F2A9AE;
    }

ipv4规则生效

使用json提交数据

表单验证通过后,提交动作默认是使用form本身的提交动作,即指定form的action和method属性:

    method="get" action=""

可以在validate()函数中添加submitHandler参数来指定点击提交后执行的函数,我们可以在该函数中使用$.json来提交数据:

    23 $("#ip_form").validate({
    24     rules: {
    25         ip: {
    26             required: true,
    27             ipv4: true,
    28         },
    29         netmask: {
    30             required: true,
    31             ipv4: true,
    32         },
    33         gateway: {
    34             required: true,
    35             ipv4: true,
    36         },
    37         dns: {
    38             dns: true,
    39         }
    40     },
    41     submitHandler: function(form) {
    42         ip_ok();
    43         $("#ip-conf").modal('hide');
    44     }
    45 });

阅读原文

迁移宁强在线步骤

配置ssh

#mkdir /root/.ssh && cp id_rsa* /root/.ssh/

下载配置脚本

#git clone git@git.oschina.net:wangsen/auto_conf_fc.git

安装基础软件

#cd auto_conf_fc && bash -x ./conf.sh

安装web软件

#yum install net-tools httpd python-django                                   
#yum install python python-pip  python-devel  python-wsgi  mod_wsgi   mariadb-server python-mysql mariadb-devel.x86_64 MySQL-python python-html5lib

克隆网站代码

#cd /var/www/html && git clone git@git.oschina.net:wangsen/TownInfo-.git
#mvTownInfo- nqys
#cd nqys && cp httpd.conf /etc/httpd/conf/httpd.conf

安装pip

#cd ~/auto_conf_fc/nqzx/ && tar xzvf pip-1.5.6.tar.gz && cd pip-1.5.6 && python get-pip.py

安装Python包

# cds && pip install -r ../requirements.txt

启动数据库

#vim /etc/my.cnf
# ## character_set_server=utf8
# systemctl enable mariadb
# systemctl restart mariadb
# mysqladmin -u root password
# mysqladmin -u root -p create nqysdb
# mysql -u root -p
##GRANT ALL PRIVILEGES ON nqysdb.* TO 'django_user'@'localhost' IDENTIFIED BY 'passw0rd'; 

替换django和registration

# cd /usr/lib/python2.7/site-packages && rm -rf django
# git clone git@git.oschina.net:wangsen/django.git
# git clone git@git.oschina.net:wangsen/registration.git

解决头像上传问题

# cd ~/auto_conf_fc/nqzx && tar xzvf Imaging-1.1.7.tar.gz
# yum install zlib zlib-devel libjpeg libjpeg-devel freetype freetype-devel
# cd Imaging-1.1.7 && vim setup.py
修改setup.py:
TCL_ROOT = "/usr/lib64/"
JPEG_ROOT = "/usr/lib64/"
ZLIB_ROOT = "/usr/lib64/"
TIFF_ROOT = "/usr/lib64/"
FREETYPE_ROOT = "/usr/lib64/"
LCMS_ROOT = "/usr/lib64/"
# pip uninstall pillow PIL
# python setup.py install

导入备份数据库

  1. 备份数据库
# mysqldump -u root -p nqysdb > nqzx.db
  1. 导入数据库
# mysql -u root -p nqysdb < nqzx.db

启动web服务

# cds && ../manage.py collectstatic
# ../manage.py syncdb
# service httpd restart

JQuery 摘要

选择符与遍历

  1. $(): $函数接受css选择符作为参数,充当一个工厂函数,返回对应元素的JQuery对象。

  2. 3种基本的选择符:

    • 标签名 $(‘p’)
    • ID $(‘#myid’)
    • 类 $(‘.myClass’)
  3. 子元素组合符>: $(‘#myid > li’)选择id为myid的元素的所有列表项(li).

  4. 否定式伪类: $(‘#myid li:not(.myClass)’)选择id为myid的元素中不属于myClass类的所有列表项(li).

  5. 属性选择符$(‘img[alt]’): 选择所有带有alt属性的img元素.

  6. 属性选择符+类正则匹配:

    • $(‘a[href^=”mailto:”]’): 选择所有URL以mailto:开头的超链接.
    • $(‘a[href$=”.pdf”]’): 选择所有URL以.pdf结尾的超链接.
    • $(‘a[href*=”rose”]’): 选择所有URL中包含rose的超链接.
  7. 自定义选择符:

    • $(‘li:eq(1)’): 选择第二个列表项
    • $(‘li:odd’): 选择奇数的列表项
    • $(‘li:even’): 选择偶数的列表项
    • $(‘li:nth-child(odd)’): 选择从父元素的第一个元素开始计算的所有奇数列表项
    • $(‘li:contain(string)’): 选择包含string的列表项
  8. 表单选择符:

    • :input
    • :button
    • :checked
    • :selected
    • :enabled
    • :disabled
  9. filter: 过滤器,可接选择器和lambda函数

    • $(‘li’).filter(‘:even’)
    • $(‘li’).filter(function(){return this.hostname && this.hostname!=location.hostname})
  10. next,nextALL,prev,prevAll,andSelf,siblings:选择所选元素的下一个元素等

  11. 连缀(chaining)

    • $(‘tr:contains(Henry)’).parent().find(‘td:eq(1)’).addClass(‘myClass’).end().find(‘td:eq(2)’).addClass(‘myClass’);
  12. $(‘#myId’).get(0) 等价于 $(‘myId’)[0]

事件

  1. window.onload vs $(document).ready

    • window.onload 是在页面的全部元素都下载好了之后才能执行,而$(document).ready()则是在DOM树准备好就可以执行了。
    • window.onload只能指定一个函数,而$(document).ready()指定的所有函数都会按顺序执行。
    • $(document).ready(func)可以简写为$(func);
  2. bind(event, func)函数可以为DOM节点绑定事件,以及事件发生时所执行的函数。event可以是:

    • blur
    • change
    • click
    • dbclick
    • error
    • focus
    • keydown
    • keypress
    • keyup
    • load
    • mousedown
    • mousemove
    • mouseout
    • mouseover
    • mouseup
    • resize
    • scroll
    • select
    • submit
    • unload
  3. toggle(func1, func2): 单击时轮流执行func1和func2

  4. toggleClass(“someclass”): 单击时轮流添加或删除someclass.

  5. 事件捕获和事件冒泡:事件捕获是从父节点开始将事件传递给子节点,而事件冒泡则正好相反。JQuery采取事件冒泡的策略。

  6. 事件对象:事件发生时执行的函数可以把事件对象作为参数。

    • event.target属性:保存发生事件的目标元素
    • event.stopPropagation(): 阻止事件继续冒泡
    • event.preventDefault(): 阻止事件的默认操作
  7. $(event.target).is(): 接收一个选择符表达式作为参数,并验证JQuery对象是否满足它。

  8. unbind(): 移除事件处理

  9. 事件命名空间:bind(‘click.sometag’, func) 可以在unbind的时候只解绑指定名字的事件。

  10. trigger(): 使用javascript去触发某个事件

效果

  1. .css(): 参数可以是(“attr”, “value”),也可以是({“attr”: “value”, “attr”: “value”}),修改JQuery对象的css
  2. .hide(): 将JQuery对象的内联css属性”display”设置为”none”
  3. .show(): 将JQuery对象的内联css属性”display”恢复成hide之前的值。hide和show可以传入速度作为参数:”slow”, “normal”, “fast”
  4. fadeIn()和fadeOut(): 淡入和淡出,可传入速度参数。
  5. slideDown()和slideUp(): 滑下和滑上,可传入速度参数。
  6. toggle(): 相当于轮流执行show()和hide()方法,可传入速度参数。
  7. slideToggle(): 相当于轮流执行slideUp()和slideDown(),可传入速度参数。
  8. animate(): 自定义动画。有两种传入参数的方式:
    • ({“attr”: “value”, “attr”, “value}, speed, easing, func):第一个参数是css属性,第二个是速度,第三个是缓动,第四个是动画完成后的回调函数。
    • ({“attr”: “value”, “attr”: “value”}, {duration: “value”, easing: “value”, complete: functiong(){}, queue: true…})
  9. outerWidth(true): 返回width+padding+border+margin的值
  10. outerWidth(), outerWidth(false): 返回width+padding+border的值
  11. innerWidth(): 返回width+padding的值
  12. outerHeight(), innerHeight()与outerWidth(), innerWidth()类似
  13. animate()中指定多个css属性变化可以让动画并发,而用多个效果方法如animate,fadeIn等连缀则可以让动画排队显示。设置queue参数为false则可以让该动画不用排队。
  14. css()方法不属于效果方法,不能排队,但可以用queue()方法将其加入队列,例如:
    • .fadeTo().queue(function(next){$(xxx).css(); next();})
  15. JQuery为每个效果方法都提供了回调函数,可以用来让多个JQuery对象的动画排队执行。

操作DOM

AJAX

使用gerrit作为代码评审工具

需求描述

其实作为项目代码的maintainer,一直习惯于mailing list + git的代码评审及管理,无奈公司主推敏捷+devops,老板让改用gerrit。硬着头皮切换到gerrit,在这里记录下安装配置的过程及踩过的许多坑,以便网友们以后配置gerrit留作参考。

需求其实很简单,我们项目一直使用公司内部一个类似于github的代码托管网站来托管项目代码,使用邮件列表来评审代码。代码通过评审通过后,我再将patch push到代码托管服务器上去。整个开发流程如下图所示:

目前开发流程

现在需要切换到gerrit来作为代码评审工具,以便于能够和jenkins集成,搭建一个集开发、构建、测试、部署为一体的devops系统,结构如下图所示。本文只关注gerrit的搭建。

CI

Gerrit简介

安装步骤

1 . 安装Java.

网上有很多安装java的博客和文章,因此在这里不再赘述,可以参考下面这篇文章:

Linux下安装java

2 . 给Gerrit单独创建一个账户

#useradd gerrit
#passwd gerrit
#su gerrit

3 . 下载gerrit

gerrit是在google上托管的项目,翻墙下载比较麻烦,可以在这里下载2.11版本的gerrit:

百度网盘下载Gerrit

将网盘中的两个文件gerrit-2.11.war以及bcpkix-jdk15on-151.jar下载到gerrit用户的家目录/home/gerrit下。

4 . 按下面的步骤安装gerrit,其中的问题大部分可以敲回车选择默认设置。


[gerrit@linux ~]$ java -jar ./gerrit-2.11.war init -d gerrit 
    
//-d指定gerrit的安装目录

Using secure store: com.google.gerrit.server.securestore.DefaultSecureStore

*** Gerrit Code Review 2.11
*** 

Create '/home/gerrit/gerrit'   [Y/n]? y

*** Git Repositories
*** 

Location of Git repositories   [git]:  

*** SQL Database
*** 

Database server type           [h2]: 

*** Index
*** 

Type                           [LUCENE/?]: 

*** User Authentication
*** 

Authentication method          [OPENID/?]: http    //为了不依赖于openid我们这里要设置成http
Get username from custom HTTP header [y/N]? y
Username HTTP header           [SM_USER]: 
SSO logout URL                 : 

*** Review Labels
*** 

Install Verified label         [y/N]? 

*** Email Delivery
*** 

SMTP server hostname           [localhost]: 
SMTP server port               [(default)]: 
SMTP encryption                [NONE/?]: 
SMTP username                  : 

*** Container Process
*** 

Run as                         [gerrit]: 
Java runtime                   [/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.45-38.b14.fc21.x86_64/jre]: 
Copy gerrit-2.11.war to /home/gerrit/gerrit/bin/gerrit.war [Y/n]? 
Copying gerrit-2.11.war to /home/gerrit/gerrit/bin/gerrit.war

*** SSH Daemon
*** 

Listen on address              [*]: 
Listen on port                 [29418]: 

Gerrit Code Review is not shipped with Bouncy Castle Crypto SSL v151
  If available, Gerrit can take advantage of features
  in the library, but will also function without it.
Download and install it now [Y/n]? n  //选择n,因为这个文件已经下载好了,安装完手动拷贝进去就行
Generating SSH host key ... rsa(simple)... done

*** HTTP Daemon
*** 

Behind reverse proxy           [y/N]? y  //需要配置反向代理来满足http认证
Proxy uses SSL (https://)      [y/N]? N
Subdirectory on proxy server   [/]: 
Listen on address              [*]: 
Listen on port                 [8081]: 
Canonical URL                  [http://null/]: 

*** Plugins
*** 

Installing plugins.
Install plugin download-commands version v2.11 [y/N]? 
Install plugin reviewnotes version v2.11 [y/N]? 
Install plugin singleusergroup version v2.11 [y/N]? 
Install plugin replication version v2.11 [y/N]? 
Install plugin commit-message-length-validator version v2.11 [y/N]? 
Initializing plugins.
No plugins found with init steps.

Initialized /home/gerrit/gerrit
Executing /home/gerrit/gerrit/bin/gerrit.sh start
Starting Gerrit Code Review: OK

安装完成后需要将下载好的bcpkix-jdk15on-151.jar文件拷贝到安装目录中去:

cp bcpkix-jdk15on-151.jar /home/gerrit/gerrit/lib/bcpkix-jdk15on-151.jar

5 . 配置Apache反向代理

修改httpd的配置文件,在其末尾加入:

<VirtualHost *>
  ServerName gerrit.example.com

  ProxyRequests Off
  ProxyVia Off
  ProxyPreserveHost On

  <Proxy *>
    Order deny,allow
    Allow from all
  </Proxy>
  <Location /login/>
    AuthType Basic
    AuthName "Gerrit Code Review"
    AuthBasicProvider file
    AuthUserFile /home/gerrit/gerrit/etc/passwords
    Require valid-user
  </Location>
  ProxyPass / http://9.181.129.109:8081/
</VirtualHost>

需要将IP地址替换成你自己的IP.修改/etc/hosts,加入gerrit.example.com的解析:

9.181.129.109	gerrit.example.com

这里也需要替换IP地址。

6 . 创建管理员账户,Gerrit把第一个被创建的用户当做是管理员账户:

$htpassword /home/gerrit/gerrit/etc/passwords $username

$username替换成你要设置的用户名。

7 . 重启服务

$./gerrit/bin/gerrit.sh restart
$sudo service httpd restart

问题总结

1 . Could not open password file.

症状:web浏览器提示服务器端错误,查看httpd日志文件/var/log/httpd/error_log,发现如下错误信息:

Permission denied:......Could not open password file: /home/gerrit/gerrit/etc/passwords

原因:该问题是由于passwords文件的路径上权限设置阻挡了httpd的访问。 解决方法:将/home/gerrit目录及passwords文件的权限设置为755

$chmod 755 /home/gerrit
$chmod 755 /home/gerrit/gerrit/etc/passwords

2 . Proxy Error

症状:输入用户名和密码登陆后,web浏览器提示代理错误。

Proxy Error

The proxy server received an invalid response from an upstream server.
The proxy server could not handle the request GET /login/.

Reason: DNS lookup failure for: 9.181.129.109:8081login

原因:最后发现是httpd的配置文件中在ProxyPass一行的IP地址后少写了一个/

ProxyPass / http://9.181.129.109:8081
--->
ProxyPass / http://9.181.129.109:8081/

参考文献

1 . Gerrit官方文档 2 . Gerrit简易安装入门

apache配置https服务

1、创建自己签名的证书

         #创建CA签名的证书,需要用到openssl
         yum install openssl    
         #创建key   
         openssl genrsa -des3 -out server.key 1024     
         #创建csr(证书签发请求) 
         openssl req -new -key server.key -out server.csr      
         #生成自己签名的证书
         openssl x509 -req  -in server.csr  -signkey server.key -out server.crt       
         #安装证书
         cp  server.crt /etc/ssl/certs
         cp  server.key /etc/ssl/private

2、编辑ssl配置文件

         vim /etc/httpd/conf.d/ssl.conf
         SSLEngine on
         SSLCertificateFile    /etc/ssl/certs/server.crt
         SSLCertificateKeyFile /etc/ssl/private/server.key

3、重启apache服务

        #systemctl restart httpd

mysql数据库及django用户名启用中文的方法

mysql数据库启用中文

在mysql的配置文件/etc/my.cnf的[mysqld]下加入

character_set_server=utf8

Django启用中文用户名

Django默认只能以字母、数字、下划线组成用户名,修改检验用户名的正则表达式可以绕过这一规则:

/usr/lib/python2.7/site-packages/django/contrib/auth/models.py:

class AbstractUser(AbstractBaseUser, PermissionsMixin):
...
validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
...

把正则表达式从^[\w.@+-]+$ 改为 ^[\S.@+-]+$即可

基于Jenkins和Koji的代码管理及构建系统

<h1>Summary</h1>

Currently, we KVM team are maintaining the code repository of the virtualization software(such as QEMU, Libvirt, Kimchi, Linux, etc.) for PowerKVM development team and delivering corresponding RPM packages. We collect patches from mailing list and build update RPM packages every workday. The work load is extremely heavy if we manually download the patches from mailing list, apply them to the repository, and build RPM packages covering all the supported platforms.

This environment, based on Jenkins and Koji, can automatically manage the code(apply the patches from mailing lists to the appropriate repository and branch) and build the required RPM packages covering all supported platforms nightly or triggered manually. By using this automation environment, developers can easily get the updated RPM packages just by sending the patches to the given e-mail address or mailing list. Furthermore, the time cost for daily routine work is dramatically reduced and the quality of the packages will be improved by eliminating human errors. This environment also can be used to other projects by making few changes to configure files.

<h1>Challenges</h1>

In PowerKVM development, we have to ship all the significant bug fixes frequently, usually a couple days. All the fix patches of QEMU, Libvirt, Linux, Kimchi will be sent to a certain mailing list. After the code cut-off day, we download all the patches from the mailing list and manually apply them to different git repositories accordingly. Then copy all the git repositories to different platform(RHEL,Fedora,etc) and do build. Last step, copy all the output RPM packages to different RPM repositories accordingly. Sometimes, we can not deliver the packages on time because the workload is extremely heavy. Furthermore, errors occur frequently such as some patches may get lost during the process due to lack of time. In order to solve these problems, we decided to make use of Jenkins and Koji to establish the CI environment.

During the construction of this environment, we have to resolve the following problems:

  • Define the rules of sending patches for developers in order to make the CI know where the patches should go.
  • Define the regular expression to distinguish patches from other e-mails.
  • How to handle exceptions.

<h1>Experience</h1>

<1>. Tools and Framework Overview

In order to implement this automation environment, several tools are needed:

  • Jenkins: Jenkins is a software that monitors executions of repeated jobs. This automation environment use Jenkins as the user interface and the manager of all the underlying jobs.
  • Koji: Koji can build RPMs packages for multiple platforms. This automation environment make use of Koji to build RPMs for all supported platform.
  • Python scripts: implement the function that collect patches from mailing list, organize and archive these patches.
  • Shell scripts: Apply the patches to the appropriate git repositories, process build and release the output RPM packages via web server.
  • Apache HTTP server: Release the RPMs via web page. Below is the chart about the framework overview of this automation environment and the relationship among these tools.

Framework

<2>. User Interface

The automation environment runs nightly. However, if the developer want to get the RPM packages immediately, he or she can simply click the start button of Jenkins. See below chart.

User Interface

<3>. Execution flow

After the developers click the first run button, jenkins will call python scripts to collect emails matching certain regular expression(We advise developers send patches to mailing list in fixed forms, such as [branch_name PATCH repo_name]xxx) and archive all the patches by date. Then, Jenkins will call shell scripts to apply these patches to the appropriate git repositories. After that, Koji will be noticed to build the RPM packages of all the supported platforms. Finally, Jenkins will call the shell scripts to copy all the RPM packages to certain directory of Apache web server.

<4>. Handle exception

The automation environment will send report containing how many patches received and where the RPM packages are published to the mailing list if all the jobs are successfully executed. If a certain job failed, it also sends report to the mailing list or email boxes configured in the configuration file. Different job has different error report message. For example, when the job who collects patches from mailing list fails, it will send out the failed patches’ name, but when the job who builds RPM packages fails, it will send out the build log.

<h1>Benefit</h1>

For the repositories maintainers, this automation environment can save much time costs for their daily routing work. What they need to do is just check reports via email clients if this automation environment start to work. The time costs dramatically reduce from several hours to a few minutes.

For the developers, they can get the RPM packages by themselves whenever they want to build one, instead of asking the maintainers spend much time to build for them. What they need to do is simply click a button.

Furthermore, the quality of the RPM packages will be improved because this automation environment can eliminate human errors effectively.

<h1>Next Steps and Recommendations</h1>

Currently, this automation environment is just used for code management and RPM packages build. Since it can save much time cost for developers and repository maintainers, we are planing to involve test cases. We can make use of automation test technology and integrate the test cases into this automation environment. After that, test engineers will be set free from burdensome test work.

发行版制作及Anaconda基础

Anaconda是用于Fedora, RHEL等Linux发行版的安装程序,可以实现通过LiveCD,PXE,NFS等方式安装Linux系统以及利用kickstart文件实现无人值守的无交互安装。

发行版制作(Fedoran系统环境)

  1. 选择所需的软件包。

制作自己的发行版首先要确定你的发行版需要安装哪些软件。决定之后需要把这些软件相应的RPM包拷贝到某个目录,然后在这个目录下执行createrepo命令来创建RPM软件源。

  1. 创建ks文件。

ks文件用于指定制作发行版时用到的软件源及软件包,具体语法可参考 kickstart语法,下面是一个简单的例子:

编译及安装

  • 获取源码:git clone git://git.fedorahosted.org/git/anaconda.git
  • 安装依赖包: sudo yum install libtool $(grep ^BuildRequires: anaconda.spec.in | awk ‘{print $2}’)
  • 安装、配置transifex:sudo yum install transifex-client;tx init /tmp
  • ./autogen.sh && ./configure && make po-pull && make

源码目录结构

  1. 接口:pyanaconda/ui/

    • gui/:图形界面接口实现代码。
    • tui/:字符界面及命令行界面实现代码。
    • _init_.py 及 common.py:定义了gui和tui通用的基类(base class)
    • communication.py:负责UI中类的通信。
  2. 自定义组件:widgets/

    • data/:存放时区地图组件的图片。
    • glade/及python/:让用户接口构建器知道组件的存在及实现python的自省。
    • src/:实现各组件。
  3. 分区:python-blivet包

  4. Bootloader: pyanaconda/bootloader.py

  5. 各个步骤的配置:

    • pyanaconda/desktop.py
    • pyanaconda/keyboard.py
    • pyanaconda/localization.py
    • pyanaconda/network.py
    • pyanaconda/ntp.py
    • pyanaconda/timezone.py
    • pyanaconda/users.py
  6. 安装软件包:

    • pyanaconda/packaging/
    • scripts/anaconda-yum
  7. 安装类: 不同的发行版可以定义不同的安装类。

    • pyanaconda/installclass.py
    • pyanaconda/installclasses/
    • pyanaconda/product.py
  8. 无人值守安装:pyanaconda/kickstart.py

  9. liveCD:

    • data/icons/
    • data/liveinst/
  10. 错误处理:

    • pyanaconda/errors.py
    • pyanaconda/exception.py
  11. 安装控制库

    • pyanaconda/install.py:控制安装步骤。
    • pyanaconda/progress.py:控制进度条。
    • pyanaconda/queue.py:控制通信队列。
    • pyanaconda/threads.py:多线程支持。
  12. 库:提供一些工具如获得用户位置,安装日志等。

    • pyanaconda/init.py
    • pyanaconda/addons.py
    • pyanaconda/anaconda_log.py
    • pyanaconda/anaconda_optparse.py
    • pyanaconda/constants.py
    • pyanaconda/flags.py
    • pyanaconda/geoloc.py
    • pyanaconda/i18n.py
    • pyanaconda/image.py
    • pyanaconda/indexed_dict.py
    • pyanaconda/isys/
    • pyanaconda/iutil.py
    • pyanaconda/nm.py
    • pyanaconda/safe_dbus.py
    • pyanaconda/simpleconfig.py
    • pyanaconda/sitecustoimze.py
  13. 主程序anaconda:由systemd在系统启动后调用,设置环境、VNC等。

  14. 启动

    • data/systemd/
    • dracut/
  15. 内存监控

    • scripts/anaconda-cleanup:监控安装过程中的内存状态,并记录到/tmp/memory.dat文件中。
    • scripts/instperf及scripts/instperf.p:利用memory.dat文件生成相应的图表。
  16. 升级工具

    • scripts/makebumpver
    • scripts/makeupdates

QEMU 2: 参数解析

#一、使用gdb分析QEMU代码#

使用gdb不仅可以很好地调试代码,也可以利用它来动态地分析代码。使用gdb调试QEMU需要做一些准备工作:

1, 编译QEMU时需要在执行configure脚本时的参数中加入–enable-debug。

2, 从QEMU官方网站上下载一个精简的镜像——linux-0.2.img。linux-0.2.img只有8MB大小,启动后包含一些常用的shell命令,用于QEMU的测试。

$wget http://wiki.qemu.org/download/linux-0.2.img.bz2
$bzip2 -d ./linux-0.2.img.bz2

3, 启动gdb调试QEMU:

gdb --args qemu-system-x86_64 -enable-kvm -m 4096 -smp 4 linux-0.2.img

-smp指定处理器个数。

#二、参数解析用到的数据结构#

QEMU系统模拟的主函数位于vl.c文件,无论是qemu-system-x86_64还是qemu-system-ppc64,都是从vl.c中的main函数开始执行。下面先介绍main函数涉及到的一些数据结构。

###QEMU链表

QEMU的链表在include/qemu/queue.h文件中定义,分为四种类型:

  • 单链表(singly-linked list):单链表适用于大的数据集,并且很少有删除节点或者移动节点的操作,也适用于实现后进先出的队列。
  • 链表(list):即双向链表,除了头节点之外每个节点都会同时指向前一个节点和后一个节点。
  • 简单队列(simple queue):简单队列类似于单链表,只是多了一个指向链表尾的一个表头,插入节点的时候不仅可以像单链表那样将其插入表头或者某节点之后,还可以插入到链表尾。
  • 尾队列(tail queue):类似于简单队列,但节点之间是双向指向的。

这里不一一介绍各种链表的用法,只通过NotifierList的定义来说明QEMU链表(list)的用法。在main函数的开头定义的DisplayState结构体使用到了NotifiereList,NotifierList就用到了链表。

a. 表头及节点的定义

定义表头需要用到QLIST_HEAD,定义如下:

 86 #define QLIST_HEAD(name, type)                                          \
 87 struct name {                                                           \
 88         struct type *lh_first;  /* first element */                     \
 89 }

NotifierList就采用了QLIST_HEAD来定义表头:

 27 typedef struct NotifierList
 28 {
 29     QLIST_HEAD(, Notifier) notifiers;
 30 } NotifierList;

定义节点需要用到QLIST_ENTRY,定义如下:

 94 #define QLIST_ENTRY(type)                                               \
 95 struct {                                                                \
 96         struct type *le_next;   /* next element */                      \
 97         struct type **le_prev;  /* address of previous next element */  \
 98 }

Notifier的节点定义如下:

 21 struct Notifier
 22 {
 23     void (*notify)(Notifier *notifier, void *data);
 24     QLIST_ENTRY(Notifier) node;
 25 };

b. 初始化表头

初始化表头用到QLIST_INIT:

103 #define QLIST_INIT(head) do {                                           \
104         (head)->lh_first = NULL;                                        \
105 } while (/*CONSTCOND*/0)

初始化NotifierList就可以这样进行:

 19 void notifier_list_init(NotifierList *list)
 20 {
 21     QLIST_INIT(&list->notifiers);
 22 }

c. 在表头插入节点

将节点插入到表头使用QLIST_INSERT_HEAD:

122 #define QLIST_INSERT_HEAD(head, elm, field) do {                        \
123         if (((elm)->field.le_next = (head)->lh_first) != NULL)          \
124                 (head)->lh_first->field.le_prev = &(elm)->field.le_next;\
125         (head)->lh_first = (elm);                                       \
126         (elm)->field.le_prev = &(head)->lh_first;                       \
127 } while (/*CONSTCOND*/0)

插入Notifier到NotifierList:

 24 void notifier_list_add(NotifierList *list, Notifier *notifier)
 25 {
 26     QLIST_INSERT_HEAD(&list->notifiers, notifier, node);
 27 }

d. 遍历节点

遍历节点使用QLIST_FOREACH或者QLIST_FOREACH_SAFE,QLIST_FOREACH_SAFE是为了防止遍历过程中删除了节点,从而导致le_next被释放掉,中断了遍历。

147 #define QLIST_FOREACH(var, head, field)                                 \
148         for ((var) = ((head)->lh_first);                                \
149                 (var);                                                  \
150                 (var) = ((var)->field.le_next))
151 
152 #define QLIST_FOREACH_SAFE(var, head, field, next_var)                  \
153         for ((var) = ((head)->lh_first);                                \
154                 (var) && ((next_var) = ((var)->field.le_next), 1);      \
155                 (var) = (next_var))

NotifierList在执行所有的回调函数时就用到了QLIST_FOREACH_SAFE:

 34 void notifier_list_notify(NotifierList *list, void *data)
 35 {
 36     Notifier *notifier, *next;
 37 
 38     QLIST_FOREACH_SAFE(notifier, &list->notifiers, node, next) {
 39         notifier->notify(notifier, data);
 40     }
 41 }

###Error和QError

为了方便的处理错误信息,QEMU定义了Error和QError两个数据结构。Error在qobject/qerror.c中定义:

101 struct Error
102 {
103     char *msg;
104     ErrorClass err_class;
105 };

包含了错误消息字符串和枚举类型的错误类别。错误类别有下面几个:

 139 typedef enum ErrorClass
 140 {
 141     ERROR_CLASS_GENERIC_ERROR = 0,
 142     ERROR_CLASS_COMMAND_NOT_FOUND = 1,
 143     ERROR_CLASS_DEVICE_ENCRYPTED = 2,
 144     ERROR_CLASS_DEVICE_NOT_ACTIVE = 3,
 145     ERROR_CLASS_DEVICE_NOT_FOUND = 4,
 146     ERROR_CLASS_K_V_M_MISSING_CAP = 5,
 147     ERROR_CLASS_MAX = 6,
 148 } ErrorClass;

QEMU在util/error.c中定义了几个函数来对Error进行操作:

error_set  //根据给定的ErrorClass以及格式化字符串来给Error分配空间并赋值
error_set_errno  //除了error_set的功能外,将指定errno的错误信息追加到格式化字符串的后面
error_copy  //复制Error           
error_is_set   //判断Error是否已经分配并设置
error_get_class  //获取Error的ErrorClass      
error_get_pretty  //获取Error的msg
error_free  //释放Error及msg的空间

另外,QEMU定义了QError来处理更为细致的错误信息:

 22 typedef struct QError { 
 23     QObject_HEAD;
 24     Location loc;
 25     char *err_msg;
 26     ErrorClass err_class;
 27 } QError;

QError可以通过一系列的宏来给err_msg及err_class赋值:

 39 #define QERR_ADD_CLIENT_FAILED \
 40     ERROR_CLASS_GENERIC_ERROR, "Could not add client"
 41 
 42 #define QERR_AMBIGUOUS_PATH \
 43     ERROR_CLASS_GENERIC_ERROR, "Path '%s' does not uniquely identify an object"
 44 
 45 #define QERR_BAD_BUS_FOR_DEVICE \
 46     ERROR_CLASS_GENERIC_ERROR, "Device '%s' can't go on a %s bus"
 47 
 48 #define QERR_BASE_NOT_FOUND \
 49     ERROR_CLASS_GENERIC_ERROR, "Base '%s' not found"
...

Location记录了出错的位置,定义如下:

 20 typedef struct Location {
 21     /* all members are private to qemu-error.c */
 22     enum { LOC_NONE, LOC_CMDLINE, LOC_FILE } kind;
 23     int num;
 24     const void *ptr;
 25     struct Location *prev;
 26 } Location;

###GMainLoop

QEMU使用glib中的GMainLoop来实现IO多路复用,关于GMainLoop可以参考博客GMainLoop的实现原理和代码模型。由于GMainLoop并非QEMU本身的代码,本文就不重复赘述。

#三、QEMUOption、QemuOpt及QEMU参数解析

QEMU定义了QEMUOption来表示执行qemu-system-x86_64等命令时用到的选项。在vl.c中定义如下:

2123 typedef struct QEMUOption {
2124     const char *name;   //选项名,如 -device, name的值就是device
2125     int flags;  //标志位,表示选项是否带参数,可以是0,或者HAS_ARG(值为0x0001)
2126     int index;  //枚举类型的值,如-device,该值就是QEMU_OPTION_device
2127     uint32_t arch_mask;  //  选项支持架构的掩码
2128 } QEMUOption;

vl.c中维护了一个QEMUOption数组qemu_options来存储所有可用的选项,并利用qemu-options-wrapper.h和qemu-options.def来给该数组赋值。赋值语句如下:

2130 static const QEMUOption qemu_options[] = {
2131     { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL },
2132 #define QEMU_OPTIONS_GENERATE_OPTIONS
2133 #include "qemu-options-wrapper.h"
2134     { NULL },
2135 };

#define QEMU_OPTIONS_GENERATE_OPTIONS选择qemu-options-wrapper.h的操作,qemu-options-wrapper.h可以进行三种操作:

QEMU_OPTIONS_GENERATE_ENUM: 利用qemu-options.def生成一个枚举值列表,就是上面提到的QEMU_OPTION_device等
QEMU_OPTIONS_GENERATE_HELP: 利用qemu-options.def生成帮助信息并输出到标准输出
QEMU_OPTIONS_GENERATE_OPTIONS: 利用qemu-options.def生成一组选项列表

可以通过下面的方法来展开qemu-options-wrapper.h来查看上述操作的结果,以生成选项为例。

  1. 在qemu-options-wrapper.h第一行写入#define QEMU_OPTIONS_GENERATE_OPTIONS.
  2. 执行命令gcc -E -o options.txt qemu-options-wrapper.h
  3. 查看文件options.txt即可

给qemu_options数组赋值后,QEMU就有了一个所有可用选项的集合。之后在vl.c中main函数的一个for循环根据这个集合开始解析命令行。for循环的框架大致如下:

  1     for(;;) {
  2         if (optind >= argc)
  3             break;
  4         if (argv[optind][0] != '-') {
  5         hda_opts = drive_add(IF_DEFAULT, 0, argv[optind++], HD_OPTS);
  6         } else {
  7             const QEMUOption *popt;
  8 
  9             popt = lookup_opt(argc, argv, &optarg, &optind);
 10             if (!(popt->arch_mask & arch_type)) {
 11                 printf("Option %s not supported for this target\n", popt->name);
 12                 exit(1);
 13             }
 14             switch(popt->index) {
 15             case QEMU_OPTION_M:
 16             ......
 17             case QEMU_OPTION_hda:
 18             ......  
 19             case QEMU_OPTION_watchdog:
 20             ......
 21             default:
 22                 os_parse_cmd_args(popt->index, optarg);
 23             }   
 24         }
 25     }

QEMU会把argv中以’-‘开头的字符串当作选项,然后调用lookup_opt函数到qemu_options数组中查找该选项,如果查找到的选项中flags的值是HAS_ARG,lookup_opt也会将参数字符串赋值给optarg。找到选项和参数之后,QEMU便根据选项中的index枚举值来执行不同的分支。

对于一些开关性质的选项,分支执行时仅仅是把相关的标志位赋值而已,如:

3712             case QEMU_OPTION_old_param:
3713                 old_param = 1;
3714                 break;

也有一些选项没有子选项,分支执行时就直接把optarg的值交给相关变量:

3822             case QEMU_OPTION_qtest:
3823                 qtest_chrdev = optarg;
3824                 break;

对于那些拥有子选项的选项,如”-drive if=none,id=DRIVE-ID”,QEMU的处理会更为复杂一些。它会调用qemu_opts_parse来解析子选项,如realtime选项的解析:

3852             case QEMU_OPTION_realtime:
3853                 opts = qemu_opts_parse(qemu_find_opts("realtime"), optarg, 0);
3854                 if (!opts) {
3855                     exit(1);
3856                 }
3857                 configure_realtime(opts);
3858                 break;

对子选项的解析涉及到4个数据结构:QemuOpt, QemuDesc, QemuOpts, QemuOptsList. 它们的关系如下图所示:

Qemu选项数据结构关系图

QemuOpt存储子选项,每个QemuOpt有一个QemuOptDesc来描述该子选项名字、类型、及帮助信息。两个结构体定义如下:

 32 struct QemuOpt {
 33     const char   *name;  //子选项的名字
 34     const char   *str;  //字符串值
 35 
 36     const QemuOptDesc *desc;  
 37     union {
 38         bool boolean;  //布尔值
 39         uint64_t uint;  //数字或者大小
 40     } value; 
 41 
 42     QemuOpts     *opts;  
 43     QTAILQ_ENTRY(QemuOpt) next;
 44 };

 95 typedef struct QemuOptDesc {
 96     const char *name;
 97     enum QemuOptType type;
 98     const char *help;
 99 } QemuOptDesc;

子选项的类型可以是:

 88 enum QemuOptType {
 89     QEMU_OPT_STRING = 0,   // 字符串
 90     QEMU_OPT_BOOL,       // 取值可以是on或者off
 91     QEMU_OPT_NUMBER,     // 数字
 92     QEMU_OPT_SIZE,       // 大小,可以有K, M, G, T等后缀
 93 };

QEMU维护了一个QemuOptsList*的数组,在util/qemu-config.c中定义:

10 static QemuOptsList *vm_config_groups[32];

在main函数中由qemu_add_opts将各种QemuOptsList写入到数组中:

2944     qemu_add_opts(&qemu_drive_opts);
2945     qemu_add_opts(&qemu_chardev_opts);
2946     qemu_add_opts(&qemu_device_opts); 
2947     qemu_add_opts(&qemu_netdev_opts);
2948     qemu_add_opts(&qemu_net_opts);
2949     qemu_add_opts(&qemu_rtc_opts);
2950     qemu_add_opts(&qemu_global_opts);
2951     qemu_add_opts(&qemu_mon_opts);
2952     qemu_add_opts(&qemu_trace_opts);
2953     qemu_add_opts(&qemu_option_rom_opts);
2954     qemu_add_opts(&qemu_machine_opts);
2955     qemu_add_opts(&qemu_smp_opts);
2956     qemu_add_opts(&qemu_boot_opts);
2957     qemu_add_opts(&qemu_sandbox_opts);
2958     qemu_add_opts(&qemu_add_fd_opts);
2959     qemu_add_opts(&qemu_object_opts);
2960     qemu_add_opts(&qemu_tpmdev_opts);
2961     qemu_add_opts(&qemu_realtime_opts);
2962     qemu_add_opts(&qemu_msg_opts);

每个QemuOptsList存储了大选项所支持的所有小选项,如-realtime大选项QemuOptsList的定义:

 507 static QemuOptsList qemu_realtime_opts = {
 508     .name = "realtime",
 509     .head = QTAILQ_HEAD_INITIALIZER(qemu_realtime_opts.head),
 510     .desc = {
 511         {
 512             .name = "mlock",
 513             .type = QEMU_OPT_BOOL,
 514         },
 515         { /* end of list */ }
 516     },
 517 };

-realtime只支持1个子选项,且值为bool类型,即只能是on或者off。

在调用qemu_opts_parse解析子选项之前,QEMU会调用qemu_find_opts(“realtime”),把QemuOptsList *从qemu_add_opts中找出来,和optarg一起传递给qemu_opts_parse去解析。QEMU可能会多次使用同一个大选项来指定多个相同的设备,在这种情况下,需要用id来区分。QemuOpts结构体就表示同一id下所有的子选项,定义如下:

 46 struct QemuOpts {
 47     char *id;
 48     QemuOptsList *list;
 49     Location loc;
 50     QTAILQ_HEAD(QemuOptHead, QemuOpt) head;
 51     QTAILQ_ENTRY(QemuOpts) next;
 52 };

其中list是同一个大选项下不同id的QemuOpts链表。

QEMU 1: 使用QEMU创建虚拟机

一、QEMU简介#

QEMU是一款开源的模拟器及虚拟机监管器(Virtual Machine Monitor, VMM)。QEMU主要提供两种功能给用户使用。一是作为用户态模拟器,利用动态代码翻译机制来执行不同于主机架构的代码。二是作为虚拟机监管器,模拟全系统,利用其他VMM(Xen, KVM, etc)来使用硬件提供的虚拟化支持,创建接近于主机性能的虚拟机。

用户可以通过不同Linux发行版所带有的软件包管理器来安装QEMU。如在Debian系列的发行版上可以使用下面的命令来安装:

sudo apt-get install qemu

或者在红帽系列的发行版上使用如下命令安装:

sudo yum install qemu -y

除此之外,也可以选择从源码安装。

##获取QEMU源码##

可以从QEMU官网上下载QEMU源码的tar包,以命令行下载2.0版本的QEMU为例:

$wget http://wiki.qemu-project.org/download/qemu-2.0.0.tar.bz2
$tar xjvf qemu-2.0.0.tar.bz2

如果需要参与到QEMU的开发中,最好使用Git获取源码:

$git clone git://git.qemu-project.org/qemu.git

##编译及安装##

获取源码后,可以根据需求来配置和编译QEMU。

$cd qemu-2.0.0 //如果使用的是git下载的源码,执行cd qemu
$./configure --enable-kvm --enable-debug --enable-vnc --enable-werror  --target-list="x86_64-softmmu"
$make -j8
$sudo make install

configure脚本用于生成Makefile,其选项可以用./configure --help查看。这里使用到的选项含义如下:

--enable-kvm:编译KVM模块,使QEMU可以利用KVM来访问硬件提供的虚拟化服务。
--enable-vnc:启用VNC。
--enalbe-werror:编译时,将所有的警告当作错误处理。
--target-list:选择目标机器的架构。默认是将所有的架构都编译,但为了更快的完成编译,指定需要的架构即可。

#二、基本原理#

QEMU作为系统模拟器时,会模拟出一台能够独立运行操作系统的虚拟机。如下图所示,每个虚拟机对应主机(Host)中的一个QEMU进程,而虚拟机的vCPU对应QEMU进程的一个线程。

QEMU结构图

系统虚拟化最主要是虚拟出CPU、内存及I/O设备。虚拟出的CPU称之为vCPU,QEMU为了提升效率,借用KVM、XEN等虚拟化技术,直接利用硬件对虚拟化的支持,在主机上安全地运行虚拟机代码(需要硬件支持)。虚拟机vCPU调用KVM的接口来执行任务的流程如下(代码源自QEMU开发者Stefan的技术博客):

open("/dev/kvm")
ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)
for (;;) {
     ioctl(KVM_RUN)
     switch (exit_reason) {
     case KVM_EXIT_IO:  /* ... */
     case KVM_EXIT_HLT: /* ... */
     }
}

QEMU发起ioctrl来调用KVM接口,KVM则利用硬件扩展直接将虚拟机代码运行于主机之上,一旦vCPU需要操作设备寄存器,vCPU将会停止并退回到QEMU,QEMU去模拟出操作结果。

虚拟机内存会被映射到QEMU的进程地址空间,在启动时分配。在虚拟机看来,QEMU所分配的主机上的虚拟地址空间为虚拟机的物理地址空间。

QEMU在主机用户态模拟虚拟机的硬件设备,vCPU对硬件的操作结果会在用户态进行模拟,如虚拟机需要将数据写入硬盘,实际结果是将数据写入到了主机中的一个镜像文件中。

#三、创建及使用虚拟机#

##命令行创建及启动虚拟机##

成功安装QEMU之后便可创建自己的虚拟机。具体步骤如下:

1, 使用qemu-img创建虚拟机镜像。虚拟机镜像用来模拟虚拟机的硬盘,在启动虚拟机之前需要创建镜像文件。

[kelvin@kelvin tmp]$ qemu-img create -f qcow2 fedora.img 10G
Formatting 'fedora.img', fmt=qcow2 size=10737418240 encryption=off cluster_size=65536 lazy_refcounts=off 
[kelvin@kelvin tmp]$ ls
fedora.img

-f选项用于指定镜像的格式,qcow2格式是QEMU最常用的镜像格式,采用写时复制技术来优化性能。fedora.img是镜像文件的名字,10G是镜像文件大小。镜像文件创建完成后,可使用qemu-system-x86来启动x86架构的虚拟机:

qemu-system-x86_64 fedora.img

此时会弹出一个窗口来作为虚拟机的显示器,显示内容如下:

QEMU虚拟机显示器输出

因为fedora.img中并未给虚拟机安装操作系统,所以会提示“No bootable device”,无可启动设备。

2, 准备操作系统镜像。

可以从不同Linux发行版的官方网站上获取安装镜像,以fedora20为例:

[kelvin@kelvin tmp]$ wget http://ftp6.sjtu.edu.cn/fedora/linux/releases/20/Live/x86_64/Fedora-Live-Desktop-x86_64-20-1.iso

3, 检查KVM是否可用。

QEMU使用KVM来提升虚拟机性能,如果不启用KVM会导致性能损失。要使用KVM,首先要检查硬件是否有虚拟化支持:

[kelvin@kelvin ~]$ grep -E 'vmx|svm' /proc/cpuinfo

如果有输出则表示硬件有虚拟化支持。其次要检查kvm模块是否已经加载:

[kelvin@kelvin ~]$ lsmod | grep kvm
kvm_intel             142999  0 
kvm                   444314  1 kvm_intel

如果kvm_intel/kvm_amd、kvm模块被显示出来,则kvm模块已经加载。最后要确保qemu在编译的时候使能了KVM,即在执行configure脚本的时候加入了–enable-kvm选项。

4, 启动虚拟机安装操作系统。

执行下面的命令启动带有cdrom的虚拟机:

[kelvin@kelvin tmp]$ qemu-system-x86_64 -m 2048 -enable-kvm fedora.img -cdrom ./Fedora-Live-Desktop-x86_64-20-1.iso

-m 指定虚拟机内存大小,默认单位是MB, -enable-kvm使用KVM进行加速,-cdrom添加fedora的安装镜像。可在弹出的窗口中操作虚拟机,安装操作系统,安装完成后重起虚拟机便会从硬盘(fedora.img)启动。之后再启动虚拟机只需要执行:

[kelvin@kelvin tmp]$ qemu-system-x86_64 -m 2048 -enable-kvm fedora.img

即可。

##图形界面创建及启动虚拟机##

命令行启动虚拟机比较繁琐,适合开发者,但对于普通用户来说,采用图形界面管理虚拟机则更为方便。采用图形界面管理QEMU虚拟机需要安装virt-manager,红帽系列的发行版只需要执行命令:

$sudo yum install virt-manager -y

安装完成后用root用户启动virt-manager:

$su -
#virt-manager

启动后的界面如下图所示:

virt-manager界面

点击左上角电脑图标即可创建虚拟机。按照步骤操作即可完成对虚拟机的创建。

Linux系统中的信号量机制

1、信号量的定义:

struct semaphore {
    spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
};

在linux中,信号量用上述结构体表示,我们可以通过该结构体定义一个信号量。

2、信号量的初始化:

可用void sema_init(struct semaphore *sem, int val);直接创建,其中val为信号量初值。也可以用两个宏来定义和初始化信号量的值为1或0:

DECLARE_MUTEX(name)	:	定义信号量name并初始化为1
DECLARE_MUTEX_LOCKED(name)	:	定义信号量name并初始化为0

还可以用下面的函数初始化:

void init_MUTEX(struct semaphore *sem);	 //初始化信号量的值为1
void init_MUTEX_LOCKED(struct semaphore *sem);	//初始化信号量的值为0

3、信号量的原子操作:

p操作:

  • void down(struct semaphore *sem); //用来获取信号量,如果信号量值大于或等于0,获取信号量,否则进入睡眠状态,睡眠状态不可唤醒
  • void down_interruptible(struct semephore *sem); //用来获取信号量,如果信号量大于或等于0,获取信号量,否则进入睡眠状态,等待信号量被释放后,激活该程。
  • void down_trylock(struct semaphore *sem); //试图获取信号量,如果信号量已被其他进程获取,则立刻返回非零值,调用者不会睡眠

v操作:

  • void up(struct semaphore *sem); //释放信号量,并唤醒等待该资源进程队列的第一个进程

4、经典同步问题的解决方案:

  • 生产者和消费者问题:

a、单缓冲区问题描述:生产者向消费者提供产品,它们共享一个有界缓冲区,生产者向其中投放产品,消费者从中取得产品。同时,每个进程都互斥的占用CPU。假定生产者和消费者是互相等效的,只要缓冲区未满,生产者就可以把产品送入缓冲区,类似的,只要缓冲区未空,消费者便可以从缓冲区中取走产品并消费它。生产者—消费者的同步关系将禁止生产者向已满的缓冲区中放入产品,也禁止消费者从空的缓冲区中获取产品

问题分析: 需要定义两个信号量,一个用于互斥访问缓冲区,另一个用于生产者与消费者之间的同步。s1=1; s2=0;

伪代码:

生产者进程            

while(1)                                         
{                                       
    printf(“I'm producing!\n”);                                        
    down_interruptible(&s1);                                         
    printf(“I'm putting a product into the buffer\n”);          
    up(&s1);                   
    up(&s2);                
}               

消费者进程

while(1)  
{
    down_interruptible(&s2);
    down_interruptible(&s1);
    printf(“I'm getting a product\n”);
    up(&s1);
    printf(“I'm consuming a product\n”);
}

b、多生产者、多消费者、n个缓冲区

问题描述:有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程并发执行,在两者之间设置了n个缓冲区,生产者将产品放入一个缓冲区中,消费者可以从一个缓冲区中取走产品去消费。要求生产者进程与消费者进程必须保持同步,即不允许生产者进程向一个满的缓冲区放产品,也不允许消费者从一个空的缓冲区取产品。

问题分析:该问题貌似比a问题复杂的多,首先我们定义一个数组buffer[n],来表示n个缓冲区,还需要定义两个变量:in 表示要存入的缓冲区的下标,out表示要取产品的缓冲区的下标。定义三个信号量:s1用于实现对缓冲池的互斥操作,empty表示空缓冲区的个数,full表示满缓冲区的个数。初值:in=out=0; s1=1; full=0; empty=n;

伪代码如下:

生产者进程                                                                  

while(1)                                                                          
{                                                                                      
    printf(“I'm producing!\n”);                                                   
    down_interruptible(&empty);                                           
    down_interruptible(&s1);                                                   
    printf(“I'm putting a product into the buffer[in]\n”);                 
                                                                                                          
    in = (in+1)%n;                                                                                
    up(&s1);                                                                                      
    up(&full);                                                                     
}                                                                                       

消费者进程

  • 哲学家进餐问题

    while(1) { down_interruptible(&full); down_interruptible(&s1); printf(“I’m getting a product from buffer[out]\n”); out = (out+1)%n; up(&empty); printf(“I’m consuming a product\n”); }

问题描述:五个哲学家共用一个圆桌,分别坐在周围的五张椅子上,在圆桌上有五只碗和五只筷子,他们交替进行思考和进餐。哲学家饥饿时便试图取最靠近他的两只筷子,当同时获得两只筷子时便可用餐,用餐完毕后放下筷子。

问题分析: 五只筷子为临界资源,定义包含五个元素的信号量数组来实现对筷子的互斥使用。chopstick[5],五个信号量的初值都为1。

伪代码:

第i(i=0,1,2,3,4)个哲学家进程:

while(1)
{
          down_interruptible(&chopstick[i]);
          down_interruptible(&chopstick[(i+1)%5]);
          printf(“I'm eating!\n”);
          up(&chopstick[i]);
          up(&chopstick[(i+1)%5);
}
  • 读者写者问题:

问题描述:一个文件可被多个进程共享,reader进程读取该文件,而writer进程负责写文件,允许多个reader进程同时读取文件,但不允许一个writer进程和其他reader进程或writer进程同时访问文件。

问题分析:进程对文件互斥访问的实现可借助一个信号量就可以搞定,但是我们需要引入一个count变量来记录reader进程的个数,对这个变量的访问也是互斥的,所以也需要引入一个信号量。定义信号量rs实现对count的互斥访问,定义ws实现对文件的互斥访问。两信号量初值都为1

伪代码:

reader进程                             	
{                                                                                          
        down_interruptible(&rs);                                                                                                                                                                                                   
        count++;
        up(&rs);  
        printf(“I'm reading!\n”);   
       down_interruptible(&rs);
       count--;
       if(count == 0){
               up(&ws);
        }
        up(&rs);
}       

 writer进程
 {
    down_interruptible(&ws);           
    if (count == 0){ 
        printf(“I'm writing!\n”);
        down_interruptible(&ws);                                     
    } 
    up(&ws);
}

基于Linux与lpc3250开发板的交叉开发环境搭建

###一、Bootloader的安装(在windows下进行)

1、什么是Bootloader:

要想弄明白什么是Bootloader,我们先从PC上的bootloader说起。PC上的BIOS和硬盘上的引导记录有着和嵌入式开发板中的bootloader类似的作用。PC的Bootloader由BIOS和MBR组成,BIOS固化在主板的一个芯片上,MBR则是硬盘的主引导扇区的缩写。PC启动后,首先执行BIOS的启动程序,根据用户的COMS设置,BOIS加载硬盘MBR的启动数据,并把系统的控制权交给保存在MBR中的OS Loader(如grub),最后再由OS Loader将控制权交给OS内核。

了解了什么是PC中的Bootloader,我们再来看什么是嵌入式系统中的Bootloader。嵌入式系统中没有与BIOS类似的芯片,这就需要开发人员自己设计Boootloader。不过,我们不必从零开始写这些代码,已经有公司和组织为大多数嵌入式系统写好了Bootloader。

2、lpc3250的Bootloader组成:

  • kickstart:位于Flash的Block0,负责加载从Flash Block1开始的程序,这里只的是S1L。开发板上电后,kickstart被内部的IROM加载并执行。IROM只能加载Block1以内的映像,而kickstart被加载后将被允许加载从Flash Block1开始的多个Block的映像文件。所以,kickstart上电后,kickstart加载S1L,也可以直接加载放在Block1的应用程序。
  • S1L:对芯片和板子进行初始化,并提供一个用于应用程序开发和执行控制的监控程序。其常用功能如下: @擦写nandFlash @串口下载,SD卡下载 @设置CPU频率 @设置从SD卡,NandFlash启动 @加载引导Eboot
  • Eboot:设置内核在NandFlash中的位置,内核复制到RAM的位置,以及内核的大小;网络下载
  • Uboot:操作系统引导。Uboot的具体分析留到以后再说。

3、安装步骤:

  • 由于笔记本不带串口,所以第一步是找个usb串口连接线,并安装好驱动。
  • 将开发板的电源线连接好,然后连接开发板串口与PC上的usb串口。
  • 利用开发板所带光盘中的smartARM3250_boot.exe安装kickstart和S1L:进入到光盘中smartARM3250_boot.exe所在的目录,确保kickstart.bin和stage1.bin也在该目录下。运行smartARM3250_boot.exe。首先选择好串口,我的是com3。如果你不知道你的串口是多少的话,可以在右键单击我的电脑——》管理——》设备管理器。查看到自己的串口位置后,点击打开串口。点击装载Bootloader,软件会提示你短接jp6,点击确定,短接jp6后reset开发板,过几秒钟便提示装载成功。这儿的bootloader不是我们上面所讲的bootloader,它指的是bootloader.bin,我们

执行了上面过程后,smartARM3250_boot.exe就将bootloader.bin拷贝到开发板执行,其实我们可以把bootloader理解成smartARM3250_boot.exe的客户端软件。

装载bootloader.bin后,我们下来就正式安装kickstart了。在右下脚的Flash擦出中选择编程NandFlash, 块地址设为0,点击选择文件,选择kickstart.bin。点击编程。这样我们就把kickstart.bin装载到了NandFlash的Block0。用同样的方法我们可以装载S1L,只是要将块地址改为1。

  • 安装U-boot或Eboot:

a. 打开windows的超级终端,配置好参数后连接。reset开发板,进入到SmartArm3250的工作台,将光盘中的u-boot.bin或eboot.nb0拷贝到一张SD卡上,然后将SD卡插入到开发板的SD插槽中,在超级终端中输入命令:load blk u-boot.bin(eboot.nb0) raw 0x83fc0000,将u-boot.bin或eboot.nb0加载到SDRAM中。 b. 在超级终端中敲入命令:nsave。将u-boot.bin(eboot.nb0)写入到NANDFlash。 c. 最后输入命令:aboot flash raw 0x83fc0000,设置从NANDFlash启动U-boot(Eboot)。

###二、Linux系统(Ubuntu)下所需要的软件的安装步骤:

1、交叉工具链的安装:

a、什么是交叉工具链:在PC机上开发嵌入式软件所需要的编译器、make等工具的集合。

b、安装步骤:

  • 将光盘中的tc-nxp-lnx-armvfp-4.3.2-1.i386.rpm拷贝到PC机的桌面。

  • 打开终端,输入sudo rpm –force-debian -ivh tc-nxp-lnx-armvfp-4.3.2.1-1.i386.rpm命令。

  • 执行上述命令后,就将交叉工具链安装到/opt/nxp目录中了,现在我们将安装路径加入到PATH变量中去。在终端中输入命令:

    gedit ~/.bashrc

在打开的.bashrc文件的末尾添加如下语句:

export PATH="$PATH:/$HOME/bin:/opt/nxp/gcc-4.3.2-glibc-2.7/bin"

保存关闭,注销当前用户,重新登录

  • 检测是否安装好交叉工具链:在终端中输入arm-vfp-linux-gnu-并按TAB键,如果能看到很多arm-vfp-linux-gnu-为前缀的命令,则说明交叉开发工具链已经安装好了。

2、NFS服务器的安装:

(NFS的详细介绍请参考NFS)

a、NFS的功能:

NFS是网络文件系统的缩写,它的功能是把NFS服务器(即Linux主机)的某个目录挂载到开发板的文件系统上(开发板上Linux系统的安装我们在后面会讲到),这样,开发板就可以执行该目录中的可执行程序。这样做的优点在于,不用将程序写入开发板的Flash,减少了对Flash的损害。

b、NFS的安装:

在Ubuntu下的安装很easy: sudo apt-get install nfs-sever

3、TFTP服务器的安装:

a、什么是tptp:TFTP是远程文件传输协议的缩写,其作用是将主机中设定目录下的文件拷贝到开发板的文件系统中,它与NFS的区别显而意见。

b、安装:还是很easy:sudo apt-get install tftp-hpa tftpd-hpa xinetd。第一个是客户端程序,第二个是服务器端程序,第三个是守护进程。

c、Ubuntu系统在安装完成后自动启动tftp服务,也可以通过命令:

sudo service xinetd start或restart命令启动。

d、然后进入xinetd.d文件夹(cd /etc/xinetd.d),查看是否有一个tftp文件,如果没有就新建一个,如果有的话就查看内容是否与下面的一致,不一致则修改,内容如下:

service tftp  
{  
    socket_type = dgram  
    wait = yes  
    disable = no  
    user = root  
    protocol = udp  
    server = /usr/sbin/in.tftpd  
    server_args = -s /home/tftpboot  
    log_on_success += PID HOST DURATION  
    log_on_failure += HOST  
}  

e、然后保存关闭。输入命令:sudo /etc/init.d/xinetd restart重启服务。

TFTP的使用我们到后面在介绍。

4、minicom的安装和配置:

a、minicom的功能:就四个字,超级终端

b、安装:sudo apt-get install minicom

c、配置:在终端输入:sudo minicom -s选择串口设置,串口设备设为/dev/ttyUSB0(也许你的不一样,可以在dev目录下查看),波特率设为115200,硬件流和软件流控制都设为No。回车退回到刚进入时的界面,选择save setup as dfl。

###三、Linux内核,安全文件系统和根文件系统的安装:

1、连接好串口线和网线。

2、插入光盘,将光盘中的uImage文件拷贝到/var/lib/tftpboot目录下。

3、在主机终端中输入:sudo chmod -R 777 /var/lib/tftpboot 命令来改变该目录权限。(这个目录是tftp服务器默认存放要传输文件的目录)

4、打开另一个终端,输入命令:sudo minicom

5、reset开发板,这时终端就进入了U-boot的工作台。

5、在工作台中输入命令:tftp 80008000 uImage将内核镜像文件拷贝到开发板内存中。

6、在工作台中输入命令:nand write.jffs2 0x80008000 0x00200000 $(filesize)将镜像文件从内存中拷贝到NANDFlash相应的位置。

7、在工作台中输入命令:setenv kernelsize $(filesize) 设置内核大小为镜像文件大小。

8、在工作台中输入命令:saveenv 保存设置

9、安装安全文件系统:

  • 将光盘中的safefs.cramfs文件放到/var/lib/tftpboot目录下。

  • 在工作台中输入命令:

    tftp 80008000 safefs.cramfs nand erase clean 0x00600000 $(filesize)
    nand write.jffs2 0x80008000 0x00600000 $(filesize)

10、将光盘中的rootfs.tar.bz2文件和burn文件拷贝到一张SD卡上。

11、将SD卡插入开发板插槽中,在工作台输入命令:run safemode。

12、坐等烧写完成,reset开发板。

ls命令实现分析

###一、ls命令的功能分析

使用man ls命令查看ls命令手册(功能描述和主要选项摘录如下):

List information about the FILEs (the current directory by default).Sort entries alphabetically if none of -cftuvSUX nor –sort.

列出文件(默认当前目录)信息,如果没有-cftuvSUX和–sort选项,就按照字母顺序排序。

-a, --all   do not ignore entries starting with .:不忽略以.开始的隐藏文件

-A, --almost-all   do not list implied . and ..:不列出 .(当前目录)和..(上级目录)

-B, --ignore-backups  do not list implied entries ending with ~:忽略以~结尾的备份文件

-c with -lt: sort by, and show, ctime (time of last modification of file status information) with -l: show ctime and  sort  by  name otherwise: sort by ctime:和-lt一起使用,则显示ctime(最后修改文件信息的时间),并按ctime排序显示;和-l一起使用,则显示
ctime,但只按文件名的字母顺序排序;其他,按ctime排序显示。/*该选项和-t选项在单独使用的时候是等价的,但在和-l选项配合使用的时候,-c的功能会被屏蔽,而-t选项不会*/

-d, --directory   list directory entries instead of contents, and do not  dereference symbolic links:不是列出该目录的文件信息,而是列出该目录项。不追踪符号链接的实际位置。

-F, --classify   append indicator (one of */=>@|) to entries:在每个entry后面加上标识文件内容的符号:

         * : 标识可执行文件     / : 标识目录          = : 套接字文件
         @ : 符号链接文件       | : 管道文件

-l     use a long listing format:以长格式显示。

###二、ls所用到的系统调用:

使用strace ls命令我们可以查看ls命令使用到的系统调用,其中最重要的几个为:

open(".", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_CLOEXEC) = 3
getdents64(3, /* 68 entries */, 32768)  = 2240
getdents64(3, /* 0 entries */, 32768)   = 0
close(3)                                = 0

1、open系统调用:

打开当前目录文件,返回获得的文件描述符。

  • O_RDONLY:只读 O_NONBLOCK:以非阻塞的方式打开文件 O_LARGEFILE:允许打开大文件
  • O_DIRECTORY:如果路径不是目录,则打开错误 O_CLOEXEC:在创建新的进程后关闭文件描述符

2、close系统调用:

关闭文件描述符。

3、getdents64:

读取当前目录下的文件。

三、getdents64的系统调用服务例程:

由于getdents64实现了ls核心功能,下面着重分析getdents64系统调用在内核态下的实现。getdents64在fs/readdir.c中定义如下:

 275SYSCALL_DEFINE3(getdents64, unsigned int, fd,
 276                struct linux_dirent64 __user *, dirent, unsigned int, count)
 277{
 278        struct file * file;
 279        struct linux_dirent64 __user * lastdirent;
 280        struct getdents_callback64 buf;
 281        int error;
 282
 283        error = -EFAULT;
 284        if (!access_ok(VERIFY_WRITE, dirent, count))
 285                goto out;
 286
 287        error = -EBADF;
 288        file = fget(fd);
 289        if (!file)
 290                goto out;
 291
 292        buf.current_dir = dirent;
 293        buf.previous = NULL;
 294        buf.count = count;
 295        buf.error = 0;
 296
 297        error = vfs_readdir(file, filldir64, &buf);
 298        if (error >= 0)
 299                error = buf.error;
 300        lastdirent = buf.previous;
 301        if (lastdirent) {
 302                typeof(lastdirent->d_off) d_off = file->f_pos;
 303                if (__put_user(d_off, &lastdirent->d_off))
 304                        error = -EFAULT;
 305                else
 306                        error = count - buf.count;
 307        }
 308        fput(file);
 309out:
 310        return error;
 311}

getdents64首先调用fget函数得到目录文件的file结构体,再调用虚拟文件系统提供的vfs_readdir函数,读取目录项,该函数的定义也在fs/readdir64中:

int vfs_readdir(struct file *file, filldir_t filler, void *buf)
  24{
  25        struct inode *inode = file->f_path.dentry->d_inode;
  26        int res = -ENOTDIR;
  27        if (!file->f_op || !file->f_op->readdir)
  28                goto out;
  29
  30        res = security_file_permission(file, MAY_READ);
  31        if (res)
  32                goto out;
  33
  34        res = mutex_lock_killable(&inode->i_mutex);
  35        if (res)
  36                goto out;
  37
  38        res = -ENOENT;
  39        if (!IS_DEADDIR(inode)) {
  40                res = file->f_op->readdir(file, buf, filler);
  41                file_accessed(file);
  42        }
  43        mutex_unlock(&inode->i_mutex);
  44out:
  45        return res;
  46}

该函数首先通过file结构体得到inode,然后从inode中获得并执行file_operations结构体中的读取目录函数(底层文件系统提供)file->f_op->readdir(file, buf, filler)。

综上所述,实际上对文件进行操作的是底层文件系统提供的函数,它通过file_operations结构体可被上层的虚拟文件系统调用,而用户程序又可通过系统调用进入内核态,调用虚拟文件系统提供的接口函数。

FUSE分析总结

###FUSE简介及原理

FUSE(Filesystem in Userspace)是sourceforge上的一个开源项目,它可以为用户提供编写用户态文件系统的接口。使用FUSE,用户可以不必熟悉Kernel代码,使用标准C库、FUSE库以及GNU C库便可设计出自己需要的文件系统。

FUSE由三个部分组成:FUSE内核模块、FUSE库以及一些挂载工具。

FUSE内核模块实现了和VFS的对接,它看起来像一个普通的文件系统模块;另外,FUSE内核模块实现了一个可以被用户空间进程打开的设备,当VFS发来文件操作请求之后,它将该请求转化为特定格式,并通过设备传递给用户空间进程,用户空间进程在处理完请求后,将结果返回给FUSE内核模块,内核模块再将其还原为Linux kernel需要的格式,并返回给VFS。如下图所示:

FUSE内核模块

图1.0 FUSE内核模块

FUSE库负责和内核空间的通信,它接收来自/dev/fuse的请求,并将其转化为一系列的函数调用,并将结果写回到/dev/fuse。

挂载工具用以实现对用户态文件系统的挂载。

FUSE的作用可通过下面两幅图说明:

Ext4文件系统的文件操作流程

图1.1 Ext4文件系统的文件操作流程

上图为Ext4文件系统的文件操作流程,当系统用户在输入ls /home/kelvin命令之后,最终会调用到Ext4文件系统内的相关函数来对文件进行处理,并将结果返回。

FUSE文件操作流程

图1.2 用户态文件系统的文件操作流程

上图是基于FUSE所写的一个用户态文件系统tfs的文件操作流程,系统用户在该文件系统(/tmp/fuse为tfs的挂载点)内所执行的ls –l /tmp/fuse命令通过FUSE最终会调用到tfs里所写的钩子函数。这样,通过FUSE,可以在用户态设计并实现自己的文件系统,而不必了解kernel的代码编写规范。

###FUSE代码编写规范

FUSE给用户提供了fuse_operations结构体,用户可实现具体的钩子函数,然后将这些钩子函数挂载到该结构体。main()函数只需调用fuse_main()就可以了,其他的工作交给FUSE去做。

FUSE代码编写框架

图1.3 用户态文件系统代码编写框架

###fuse_main()的处理流程

fuse_main()函数处理流程

图1.4 fuse_main()函数的处理流程

fuse_main()被调用后,它调用fuse_mount(),创建新的进程fusermount,来检查FUSE内核模块是否加载,并返回文件描述符给fuse_main()。fuse_new()为文件系统分配数据空间。fuse_loop()从/dev/fuse 读取文件系统调用,调用fuse_operations结构中的处理函数,返回调用结果给/dev/fuse。

使用FUSE的注意事项

  • FUSE的作用在于使用户能够绕开内核代码来编写文件系统,可文件系统如果要实现对具体的设备的操作的话必须要使用设备驱动提供的接口,而设备驱动位于内核空间,FUSE便无法将文件系统挂载到具体设备上去。所以,基于FUSE所写的文件系统通常是将文件当做虚拟的磁盘,并使用C所提供的文件操作接口;或者是映射一个目录到文件系统。

  • FUSE给各钩子函数传递的path参数的/指的是文件系统的/目录。

NBD-网络块设备[翻译]

1997年4月,Pavel Machek 写了他的网络块设备代码,并被当时的Linux Kernel 2.1.55接受。Pavel 在随后的四个发行版(对应的内核版本为55、101、111、132)中维护并升级了他的代码。Andrzej M. Krzysztofowicz贡献了64位机上运行的版本,随后Stephen Tweedie 为其提出了许多的专业建议,尤其是引入了基于信号量的锁机制,使得代码在对称多处理器系统中能够安全的运行。作者们已将其增强以便于运用于工业环境之中。本文描述了网络块设备、驱动、以及它的开发历史。

NBD的驱动程序提供了一种在这个面向网络的世界中更为普遍的访问模型。它在本地客户端模拟了一个块设备,比如一个磁盘或者是一块磁盘分区,但实际提供物理支持的却是通过网络连接的远程服务器。在本地,该设备看起来是一个磁盘分区,但那只是远程服务器的表面。远程服务器是一段轻量级的守护进程代码,它提供对远程设备的访问,而且该守护进程并不需要运行在linux平台之上。本地操作系统必须是linux,而且要支持Linux内核NBD驱动,并且能够运行本地客户端进程。安装NBD可被用来进行远程存储和备份,而且能用来在地球的任意一个角落虚拟地传送物理设备。

NBD在本地客户端代表远程资源

图1.NBD在本地客户端代表远程资源

NBD有一些UNIX系统组件的经典特征:简单、小巧、多功能。文件系统能被挂载到NBD上(图1),而且NBD设备也能够用来组成冗余磁盘阵列(RAID)。在本地Linux上挂载一个EXT2文件系统到NBD,其传输率要比在同样的远程机器上挂载一个NFS快(表1是接近Pavel最初写的驱动代码的计时)。

在此输入图片描述

图2.使用NBD的冗余磁盘阵列

write cmd.	dd=if=/dev/zero of=/dev/nd0 bs=1024 count=32000	
NBD bs=1K	0.04user 5.54system 0:26:67elapsed 20.9%CPU	1.23MBps
write cmd.	dd=if=/dev/zero of=/dev/nfs bs=1024 count=32000	
NFS r/wsize=1K	0.15user 7.46system 0:43.58elapsed 17.0%CPU	0.75MBps
write cmd.	dd=if=/dev/zero of=/dev/nfs bs=4096 count=8000	
NFS r/wsize=4K	0.03user 3.94system 0:35.18elapsed 11.0%CPU	0.93MBps

表 1. NBD与NFS在一个端口上的写传输速率比较

挂载到NBD上的Linux EXT2文件系统,缓冲区大小接近1.5KB,其性能赶得上默认条件下的NFS。也就是说,它的大小是以太网默认传输单元(MTU/MRU)的大小——1.5KB。这恰好是NFS默认缓冲区大小(1KB)的1.5倍。NBD因为使用TCP而不是UDP作为传输协议而具有很好的恢复力。TCP包含它固有的一致性以及恢复机制,因而比UDP协议庞大。但对NBD来说,TCP的开销会被它节省的重新传输和校正码所需带宽抵消。

NBD能够被用作一个中型邮件服务器A的实时镜像。故障转移可以通过在另一个房间里放置一台用100BT网络连接的备份服务器B来实现。NBD设备将主服务器和备份服务器连接,并提供了主服务器RAID-1镜像的一半(Y),另一般是主服务器自己的用于存放邮件的分区(X)。组合成的设备XY将作为mail spool被挂载。

当服务器A失效时,运行在B上的一个守护进程能够探测到NBD连接的断开,然后检查镜像Y的mail spool, 校验最小的不完整性,并将其在本地作为mail spool挂载,最后在B上启动A广播的邮件交换IP地址的别名。当A修复之后,会被守护进程检测到,别名将会被丢弃。NBD连接随后会被重新修复,A上的分区X将会重新和NBD镜像Y保持同步。主服务器RAID-1设备会被重新调度,NBD镜像也会重新从属于它。邮件服务将会以默认的配置恢复。使主从服务器设置对称是可能的,但不适宜在这里描述细节。

与其他备选方案相比,这种方法具有许多优点。其一,比如说,维护一个空的邮件服务器,在主服务器宕机的时候将其唤醒。这会使得许多已经放到spool中的邮件在停机期间不可被使用。但重新整合是很容易的,将相关的文件连接到恢复后的服务器也很简单。但主服务器上的某些邮件可能会丢失。

另一种解决办法是将邮件服务器分散到一些不同的机器上,这些机器通过NFS挂载同样的spool区域。然而,NFS的文件锁过去从未被证明是完全可靠的,NFS也从未令人满意的支持由于多个用户同时选择下载50MB的邮件目录引起的事件的激增。传输率也会随着文件尺寸成指数地变慢。在不利的网络条件下,软模式的传输率极有可能崩溃。如果NFS在服务启动的时候为了追求高的可靠性而以同步(硬)模式挂载,一旦NFS服务失效,将会导致所有客户端停机。NFS服务不得不独立地建立镜像。