在pcDuino上通过Cylon控制硬件

May 17th, 2016 by JasonLe's Tech 1,038 views

准备工作:

我们用的板子型号是pcDuino3B(http://www.linksprite.com/linksprite-pcduino3/)。默认系统是ubuntu 12.04。推荐从官网下载固件(http://www.linksprite.com/linksprite-pcduino3/)把pcDuino刷到buntu 14.04,因为nodejs发展迭代比较快,低版本的gcc,g++无法编译某些nodejs。

安装具体的构建工具:

1. armhf的源非常少,所以例如sohu,163很多arm设备的源根本不支持,推荐原装的源,可能网速慢些,但起码比较齐全。
2. apt-get install build-essential
3. apt-get install git
4. ubuntu 12.04 的软件仓库中版本比较老,从仓库直接安装nodejs和npm后,安装grunt会存在问题,所以推荐源码安装,或者直接官网armv7二进制版本安装。
5. wget https://nodejs.org/dist/v6.1.0/node-v6.1.0-linux-armv7l.tar.xz
6. 解压缩node-v6.1.0-linux-armv7l.tar.xz
7. cd node-v5.10.1-linux-armv7l
8. 将其复制到/usr/local下即可使用
9. 查看node 版本 :node -v
10. 查看npm版本:npm -v
11. 安装好npm后,设置国内npm源:npm config set registry https://registry.npm.taobao.org

在安装过程中,我遇到很多坑,比如安装node-inspector,最后返回 ELIFECYCLE,通过查找资料,我们可以使用–unsafe-perm,对于一些在armv7上没有编译的二进制包,我们可以使用–build-from-source,直接本地编译(尽量使用g++ 4.8之后的版本)。

开始写代码:

比如我们在一个文件中写入:

'use strict';

    console.log("hello test");
    var Cylon = require("cylon");

    Cylon.robot({
      connections: {
        pcduino: { adaptor: 'pcduino' }
      },

      devices: {
        led: { driver: 'led', pin: 10 }
      },

      work: function(my) {
       every((1).second(), my.led.toggle);
      }
    }).start();

然后通过node运行,查看安装效果:

QQ图片20160517214206

目前已有很多项目在使用这个serialport包进行串口处理:

  • Johnny-Five – Firmata based Arduino Framework.
  • Cylon.js – JavaScript Robotics, By Your Command.
  • node-l8smartlight (source) A node library to control the L8 Smartlight via Bluetooth or USB port
  • firmata Talk natively to Arduino using the firmata protocol.
  • tmpad source – a DIY midi pad using infrared, arduino, and nodejs. Video
  • duino – A higher level framework for working with Arduinos in node.js.
  • Arduino Drinking Game Extravaganza – AKA “The Russian” a hexidecimal drinking game for geeks by Uxebu presented at JSConf EU 2011.
  • Arduino controlling popcorn.js – Controlling a popcorn.js video with an Arduino kit.
  • Robotic JavaScript – The first live presentation of the node-serialport code set as presented at JSConf EU 2010.
  • devicestack – This module helps you to represent a device and its protocol.
  • reflecta A communication protocol that combines Arduino Libraries and NodeJS into an integrated system.
  • rc4pt-node – Control Popcorntime with an Infrared receiver and Arduino.

参考

http://www.linuxidc.com/Linux/2016-04/130093.htmnodejs

官方代码仓库:https://nodejs.org/download/release/latest/

配置淘宝npm源:http://www.cnblogs.com/trying/p/4064518.html

http://cnodejs.org/topic/4f9904f9407edba21468f31e

试玩Chromebook Piexl

April 12th, 2016 by JasonLe's Tech 991 views

Chromebook piexl是Google在2013年新出的一款超极本,他的外形十分小巧,内置Chorme OS系统,用户的操作处于云端,但是基于国内大家都懂得的网络环境,这款笔记本犹如鸡肋。配置清单如下:

Model Pixel (Wi-Fi)[19] Pixel (LTE) [19] Pixel (2015) [20] Pixel (LS) [20]
Release date February 2013 April 2013[21] March 2015 March 2015
Release price US$1299 US$1449 US$999 US$1299
Size 297.7 × 224.6 × 16.2 mm 297.7 × 224.55 × 15.3 mm
Weight 1.52 kilograms (3.4 lb) 1.5 kilograms (3.3 lb)
Processor Intel Core i5-3427U (dual-core 1.8 GHz) (CPU)
Intel HD Graphics 4000 (integrated) (GPU)
Intel Core i5-5200U (dual-core 2.2 GHz) (CPU)
Intel HD Graphics 5500 (integrated) (GPU)
Intel Core i7-5500U (dual-core 2.4 GHz) (CPU)
Intel HD Graphics 5500 (integrated) (GPU)
Memory 4 GB DDR3 RAM 8 GB DDR3 RAM 16 GB DDR3 RAM
Storage 32 GB Solid state 64 GB Solid state 32 GB Solid state 64 GB Solid state
Screen 12.85 in (326 mm)
2,560 × 1,700 (239 ppi)
3:2 (1.5:1) aspect ratio
400 nit brightness
178° viewing angle
Multi-touch
Gorilla Glass
Webcam 720p HD, integrated
Keyboard Backlit
Touchpad Clickable, etched-glass
Audio 3.5-mm combo headphone/microphone jack
3 built-in microphones
Integrated DSP (for noise cancellation)
Stereo speakers
Ports 2 × USB 2.0
Mini DisplayPort
SD/MMC card reader
2 × USB 3.1 Type-C (5Gbit/s data, power in, video out)
2 × USB 3.0 Type-A
SD/MMC card reader
WiFi 802.11a/b/g/n
Dual-band (2.4/5 GHz)
2×2 MIMO
802.11a/b/g/n/ac
Dual-band (2.4/5 GHz)
2×2 MIMO
Bluetooth Bluetooth 3.0 Bluetooth 4.0
Wireless LTE modem
Battery 59 Wh (5 hours active use) 72 Wh[22] (12 hours for average user behaviour)[22]
Included extras 1 TB Google Drive storage for 3 years
12 sessions GoGo Internet
100 MB/month free fromVerizon (US) (LTE only)
1 TB Google Drive storage for 3 years12 sessions GoGo Internet

我拿到手的是WiFi版本,可以看出CPU和Memeory在当时都算是主流本,唯一的问题就是内置的SSD太小了,只有32GB,装Windows肯定是不够的,所以我只好装了Ubuntu 14.04 LTS。默认情况Chromebook处于用户模式下,用户无权对系统进行修改,所以我们要先切换到开发者模式下:

  1. 同时按住Esc,F3和电源键,系统会自动重启,进入开发者模式
  2. 重启后,屏幕会显示系统处于开发者模式,不用管。直接Ctrl+D

18od6wnzwo855jpg

3. 这时已经进入系统(需要搭梯子)

Chromebook安装ubuntu有两种工具1.Crouton  2.硬盘安装。我个人喜欢硬盘安装,可以十分有效率的控制硬件。具体Crouton安装,可以参考:http://lifehacker.com/how-to-install-linux-on-a-chromebook-and-unlock-its-ful-509039343

硬盘安装的方式可能需要修改一些配置,因为Chromebook的SeaBios绑定了引导系统,所以我们必须关闭bios的安全引导功能,然后才可以安装启动ubuntu。首先我们打开终端Ctrl+Alt+T ,然后shell,然后运行sudo bash,这时我们就对系统有完整的控制权。这时我们

$crossystem
$crossystem dev_boot_usb=1
$crossystem dev_boot_legacy=1

然后重启,插入Ubuntu 安装盘,这时会进入Seabios,无视之,按下Ctrl+L && Esc即可。下面安装就大同小异了,推荐安装新版本内核,因为Chrombook拥有触摸屏和触摸板,在旧版本内核下可能无法使用,安装linux 4.0后的版本就很好的驾驭了,来张安装完成图:

IMG_1696

Chromebook只适合上网办公轻应用,遇到播放flash视频时,散热量非常大可能比较影响体验。

参考:

https://gbatemp.net/threads/how-to-install-windows-8-on-your-google-chromebook-pixel.350111/

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

March 17th, 2016 by JasonLe's Tech 945 views

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 by JasonLe's Tech 1,932 views

最近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

setjmp和longjmp的另类使用

February 3rd, 2016 by JasonLe's Tech 1,018 views

C语言的运行控制模型,是一个基于栈结构的指令执行序列,表现出来就是call/return: call调用一个函数,然后return从 一个函数返回。在这种运行控制模型中,每个函数调用都会对应着一个栈帧,其中保存了这个函数的参数、返回值地址、局部变量以及控制信息等内容。当调用一个 函数时,系统会创建一个对应的栈帧压入栈中,而从一个函数返回时,则系统会将该函数对应的栈帧从栈顶退出。正常的函数跳转就是这样从栈顶一个一个栈帧逐级地返回。

setjmp的返回值:直接调用该函数,则返回0;若由longjmp的调用,导致setjmp被调用,则返回val(longjmp的第二个参数)。

之前看APEU的相关章节,setjmp和longjmp只是一个跨函数跳转的库函数调用,可以作为后悔药使用,但是今天我发现这个库函数可以作为协程使用。协程我之前一直不理解,认为有了进程线程就可以了,没有必要存在协程,但是发现在不支持这些多线程多进程的操作系统平台上协程意义重大。

这个时候协程就可以派上用场了,我们可以依赖协程模拟多进程这种需求,我们需要写一个thread库供协程调用,具体的thread工作步骤就是:

  1. 存储当前线程所在上下文,设置一个存储队列专门存储thread context
  2. 为每个线程分配一个stack空间
  3. 将esp指向栈顶,eip指向要执行代码的entry,当然包括参数arg,arg具体调用方式就是(current->entry(current->arg)),这一个非常相似于c++中的委托
  4. 当需要调度线程时,将当前执行代码设置setjmp,保存线程结构体中的thread context到具体全局的数组
  5. 如果需要调度另外一个线程,使用longjmp跳入到线程结构thread context

当然了在linux下有glibc提供相关库函数实现跳转,咱们不必再次造轮子,但是在裸机上,或者一种新的体系结构中,我们必须自行实现setjmp和longjmp,这其中不可避免的会使用到asm。比如setjmp,首先要将返回地址和frame pointer压入栈,考虑到栈自高地址向低地址方向生长,故esp-8,然后再压入其他通用寄存器。而longjmp恢复某个线程·的上下文环境,必须指定存储context位置 ,然后将返回地址复制给eax,然后执行跳转。

struct jmp_buf
{
       unsigned j_sp;  // 堆栈指针寄存器
       unsigned j_ss;  // 堆栈段
       unsigned j_flag;  // 标志寄存器
       unsigned j_cs;  // 代码段
       unsigned eip;  // 指令指针寄存器
       unsigned ebp; // 基址指针
       unsigned edi;  // 目的指针
       unsigned j_es; // 附加段
       unsigned j_si;  // 源变址
       unsigned j_ds; // 数据段
};

具体线程切换伪代码:

void wthread_yield()
{
   ...
   if(current){
        if(setjmp(current->...)!=0)
             return;
        push(...)
   }
   current = next;
   longjmp(current->...)
}

考虑到执行setjmp和longjmp必须是一个控制main线程,必须由控制线程控制调用线程切换,其他线程可以主动让出时间片。这时我们必须定义一个全局变量保存线程上下文,然后维护这个数组,至于具体的逻辑形式可以是队列可以是环形队列队列等。编写thread库务必保证线程安全,不能破坏线程返回地址,否则容易core dump。

另外在linux下,可以使用这两个系统调用实现C下的异常处理try/catch,至于在setjmp和longjmp之前存在的变量务必使用volatile声明。

 

 参考:

http://stackoverflow.com/questions/2560792/multitasking-using-setjmp-longjmp#comment33335405_2560792
http://www.cnblogs.com/lq0729/archive/2011/10/23/2222117.html
http://www.cnblogs.com/lienhua34/archive/2012/04/22/2464859.html