存档

文章标签 ‘linux’

给 Mac 一个 ssh-copy-id

2014年12月25日 没有评论

Mac 下默认是没有这个非常好用的工具的, 最开始我每次手动把 ~/.ssh/id_rsa.pub 的 内容追加到服务器的 ~/.ssh/authorized_keys 文件中.

后来我自己写了一个脚本, 放到可执行目录中, 爽了很多:

#!/bin/bash
 
# file: ssh-copy-id
 
if [ $# -lt 1 ]; then
    echo 'Usage: ssh-copy-id [user1@]hostname1 [user2@]hostname2]'
fi
 
KEY_FIEL="$HOME/.ssh/id_rsa.pub"
if [ -f $KEY_FIEL ]; then
    key=`cat $KEY_FIEL`
    for server in $@; do
        ssh $server "echo $key >> ~/.ssh/authorized_keys"
    done
else
    echo 'ssh key(id_rsa.pub) does not exist, run ssh-keygen to generate it'
    exit 1
fi

在后来, 我偶然发现 brew 中直接有一个包就叫 ssh-copy-id, 我靠, 一直被 linux 系统给误导了, 在 linux 系统中, ssh-copy-id 是包含在名为 openssh-clients 的包中的. 废话不说, 用这个吧, 毕竟人家写的 300 多行, 考虑了各种兼容性等等

$ brew install ssh-copy-id

分类: linux 标签:

CentOS 7 修改时区

2014年12月25日 没有评论

Linux 系统(我特指发行版, 没说内核) 下大部分软件的风格就是不会仔细去考虑向后 的兼容性, 比如你上个版本能用这种程序配置, 没准到了下一个版本, 该程序已经不见了. 比如 sysvinit 这种东西.

设置时区同样, 在 CentOS 7 中, 引入了一个叫 timedatectl 的设置设置程序.

用法很简单:

# timedatectl # 查看系统时间方面的各种状态
      Local time: 四 2014-12-25 10:52:10 CST
  Universal time: 四 2014-12-25 02:52:10 UTC
        RTC time: 四 2014-12-25 02:52:10
        Timezone: Asia/Shanghai (CST, +0800)
     NTP enabled: yes
NTP synchronized: yes
 RTC in local TZ: no
      DST active: n/a
# timedatectl list-timezones # 列出所有时区
# timedatectl set-local-rtc 1 # 将硬件时钟调整为与本地时钟一致, 0 为设置为 UTC 时间
# timedatectl set-timezone Asia/Shanghai # 设置系统时区为上海

其实不考虑各个发行版的差异化, 从更底层出发的话, 修改时间时区比想象中要简单:

# cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

分类: linux 标签:

使用 O_DIRECT 需要注意的地方

2014年4月30日 没有评论

在之前的 open 使用标记里面, 我提到了 O_DIRECT 标志的使用, 使用 DMA 的方式,
数据不经过内核空间, 直接在用户空间和设备之间传输. 在文章的测试例子里面我放了一个
小错误, 在使用这个标志时读写文件时没有对相关的参数对齐.

实际上, 使用 O_DIRECT 打开的文件要求读写的 bufferbuffer_size 和读写偏移
都要做 I/O 对齐, 对齐的单位为 logical_block_size, 是存储设备能寻址的最小存储
单元, 可以用过下列指令查看该值:

# cat /sys/block/sda/queue/logical_block_size 
512

buffer_size 和偏移的对其都比较好处理, 但是 buffer 地址的对其不太方便, 不过
glibc 提供了 posix_memalign() 函数, 可以返回一个对齐后的 buffer.

下面是使用的小例子:

unsigned char buf[512] = "1234567890";
void *align_buf = NULL;
 
/* 假设 /sys/block/sda/queue/logical_block_size 为 512B */
if (posix_memalign(&align_buf, 512, sizeof(buf)) != 0) {
    perror("memalign failed");
    return;
}
int len = pwrite(fd, align_buf, sizeof(buf), offset);
/* ... ... */

分类: Linux 标签:

利用 API 获取 CPU 和内存信息

2013年9月1日 没有评论

今天在研究 CPU 的热插拔时,看到了获取系统 CPU 和内存信息的 API, 才发现以前在实现 这些功能的时候去 /proc 获取数据是多么的 ugly.

这些 API 主要是利用了 sysconf 这个 POSIX 的接口, 这个接口可以获取系统运行 时信息, 包括 CPU 信息, 内存信息, 进程可以打开的最大文件句柄数等. 它的声明如下:

long sysconf(int name);

  • _SC_NPROCESSORS_CONF: 获取系统中总的 CPU 数量, 注意这里获取的是所有的 CPU 线程的数量
  • _SC_NPROCESSORS_ONLN: 获取系统中可用的 CPU 数量, 没有被激活的 CPU 则不统计 在内, 例如热添加后还没有激活的.
  • _SC_PHYS_PAGES: 总的物理内存页大小.
  • _SC_AVPHYS_PAGES: 可用的物理内存页大小.

下面是我写的一些 demo 代码(在下面的代码里把 windows 获取这新信息的方法也写出来做参考):

#include<stdio.h>
 
#if defined(_WIN32)
#define _WIN32_WINNT 0x0500
#include <windows.h>
void sysinfo_print()
{
    int cpu_num;
    SYSTEM_INFO si;
    MEMORYSTATUSEX memory;
 
    // 大部分 Windows 系统不支持热添加功能, 所以 online number 没有什么意义.
    GetSystemInfo(&si);
    cpu_num = si.dwNumberOfProcessors;
    printf("The number of processors: %d\n", cpu_num);
 
    memory.dwLength = sizeof(memory);
    GlobalMemoryStatusEx(&memory);
    printf("The memory size: %I64uK\n", memory.ullTotalPhys/1024);
    printf("The free memory size: %I64uK\n", memory.ullAvailPhys/1024);
}
#else
#include<unistd.h>  
#include<errno.h>
#include <string.h>
void sysinfo_print()
{
    int cpu_num, cpu_online_num;
    int mem_size, mem_free_size;
 
    cpu_num = sysconf(_SC_NPROCESSORS_CONF);
    if (cpu_num != -1) {
        printf("The number of processors: %d\n", cpu_num);
    } else {
        printf("Failed to get number of processors: %s\n", strerror(errno));
    }
 
    cpu_online_num = sysconf(_SC_NPROCESSORS_ONLN);
    if (cpu_online_num) {
        printf("The number of online processors: %d\n", cpu_num);
    } else {
        printf("Failed to get number of online processors: %s\n",
               strerror(errno));
    }
 
    // 注意: OSX 不支持下面两个宏.
    mem_size = sysconf(_SC_PHYS_PAGES);
    if (mem_size) {
        printf("The memory size: %dK\n", mem_size * 4);
    } else {
        printf("Failed to get memory size: %s\n", strerror(errno));
    }
 
    mem_free_size = sysconf(_SC_AVPHYS_PAGES);
    if (mem_free_size) {
        printf("The free memory size: %dK\n", mem_free_size * 4);
    } else {
        printf("Failed to get free memory size: %s\n", strerror(errno));
    }
 
}
#endif
 
int main(int argc, char *argv[])
{
    sysinfo_print();
 
    return 0;
}

分类: linux, programming 标签: ,

open 的同步标记

2012年8月13日 3 条评论

open 是 Linux 下打开文件的标准 API, 这个 API 同时定义了很多文件操作的参数, 不同的参数对性能影响很大. 事实上, 对同步的参数来讲(O_SYNC 系列), 默认的参数 很快, 但是会损失一些功能, 比如 cache 的存在并没有真正的把修改的内容写入文件, 如果是异常关机可能导致磁盘数据和内存没有同步, 对某些应用(如虚拟机)意味着磁盘 可能损坏.

O_SYNC

同步 I/O 的标记, 执行 write 时, 保证数据被写入硬件才返回, 也就是说调用 write 写数据的时候, write 将被阻塞, 直到所有数据(包括文件内容和文件属性) 都写入了底层硬件.

O_DSYNC

和 O_SYNC 类似, 只不过这个标记保证的只是文件的内容被写入底层硬件, 并不保证 文件属性的同步.

O_DIRECT

POSIX 并没有包含这个标志, 在 Linux 上使用的时候必须要定义 _GNU_SOURCE 这个宏. 如果使用这个标志, 数据的传输将不会通过内核空间, 而是使用 DMA 直接在用户空间 到存储设备之间传送, 不保证数据是否同步. 所以 O_DIRECT 常和 O_SYNC 一起 保证数据的同步和效率.

测试

我写了一个简单的测试程序来测试以上参数对数据读写的影响.

测试结果
Flags Used Time
Default 3ms
O_SYNC 90000ms
O_DSYNC 30000ms
O_DIRECT 1ms
O_SYNC and O_DIRECT 1ms

测试代码:

#define _GNU_SOURCE
 
#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
#include 
#include 
#include 
#include 
 
/* #define USE_SYNC 1 */
/* #define USE_DSYNC 1 */
/* #define USE_DIRECT 1 */
 
void test()
{
    int fd;
    int i;
    int offset = 0;
    unsigned char buf[512] = "1234567890";
    int flags = 0;
 
#ifdef USE_DIRECT
    void *align_buf = NULL;
    if (posix_memalign(&amp;align_buf, 512, sizeof(buf)) != 0) {
        perror("memalign failed");
        return;
    }
#endif
 
    flags = O_WRONLY | O_CREAT | O_TRUNC;
#ifdef USE_SYNC
    printf("USE O_SYNC flag\n");
    flags |= O_SYNC;
#endif
#ifdef USE_DSYNC
    printf("USE O_DSYNC flag\n");
    flags |= O_DSYNC;
#endif
#ifdef USE_DIRECT
    printf("USE O_DIRECT flag\n");
    flags |= O_DIRECT;
#endif
    fd = open("/tmp/test.bin", flags, 0644);
 
    if (fd == -1) {
        perror("Create file failed");
        return;
    }
    if (ftruncate(fd, 10 * 1024 * 1024)) {
        goto cleanup;
    }
 
    for (i = 0; i &lt; 2048; ++i) {
#ifdef USE_DIRECT
        int len = pwrite(fd, align_buf, sizeof(buf), offset);
#else
        int len = pwrite(fd, buf, sizeof(buf), offset);
#endif
    if (len &lt; 0) {
            perror("failed to write");
            break;
    } else if (len == 0) {
            break;
            /* ? */
        }
        offset += len;
    }
 
cleanup:
    if (fd != -1) {
        close(fd);
    }
#ifdef USE_DIRECT
    if (align_buf) {
        free(align_buf);
    }
#endif
}
 
int main(int argc, char *argv[])
{
    test();
    return 0;
}
分类: linux 标签:

在 QEMU 上使用 KGDB 调试内核

2011年12月28日 3 条评论

最近在研究 Linux Kernel, 由于我看问题喜欢直接看本质, 所以直接从代码开始看起,
但是 Linux 发展到现在代码何其多, 何其复杂, 里面的流程, 逻辑, 甚至各种变量绝对不是
我以前开发的项目能比的, 比如说里面全局变量的大量使用, 各种 goto 的使用,
所以必须要有一个好的阅读方法和好的阅读手段. 阅读代码的 Emacscscope 足以.
但是对于习惯 gdb 调试的我, 还是希望可以利用 gdb 的强大优势帮助学习. 在加上
QEMU 来作为 kernel 的运行平台, 这样的组合不事半功倍都说不过去.

内核构建

下载内核

经我测试 linux-2.6.24 和 linux-2.6.25 没有 KGDB 的支持. 为了能有 KGDB 的
支持, 我选择版本稍微高一点内核.

# mkdir -p ~/Develop/Linux && cd ~/Develop/Linux
# wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2
# tar xf linux-2.6.34.tar.bz2

配置内核

# cd ~/Develop/Linux/linux-2.6.34 
# make defconfig # 用 defconfig 生成 一个精简内核 
# make menuconfig

确保下面的被选中

 General setup  ---> 
     [ * ] Prompt for development and/or incomplete code/drivers
Kernel hacking  --->
     [ * ] Compile the kernel with debug info
     [ * ] Compile the kernel with frame pointers
     [ * ] KGDB: kernel debugger  --->
           < * >   KGDB: use kgdb over the serial console

OK, 下面开始编译内核

# make -j5 # 因为我是 4 核的 CPU, 所以这里使用5个线程同时并发执行

QEMU 构建

安装 QEMU

这里可以选择多种安装方式, 可以选择从源码安装或者从发行版的二进制安装.
作为一个比较喜欢追根究底的 geek, 我选择的是源码安装.

另外, 我调试的只是 kernel, 所以没有硬件虚拟话我是可以忍受的,
所以我把 kvm 从编译参数那里去掉了.

# mkdir -p ~/Develop/QEMU
# cd ~/Develop/QEMU
# wget http://wiki.qemu.org/download/qemu-1.0.tar.gz
# tar xf qemu-1.0.tar.gz
# cd qemu-1.0
# ./configure --prefix=./ --target-list="i386-softmmu x86_64-softmmu" --disable-kvm
# make -j3 && make install

注意, 因为我不想把 qemu 和我系统的 qemu 冲突, 我简单的将 qemu
安装在 qemu 源码目录下.

文件系统构建

安装 busybox

# mkdir ~/Develop/busybox
# wget http://www.busybox.net/downloads/busybox-1.19.3.tar.bz2
# tar xf busybox-1.19.3
# cd busybox-1.19.3
# make menuconfig

静态编译的选择很重要, 如果不选择的话, 需要把 libc.so 和 ld.so 等
复制到文件系统里面, 稍微麻烦一些, 这里我们选择最简单的方式.

另外这个版本静态编译的时候 mount umount会出错, 方正对我来说不需要,
我暂时去掉.

Busybox Settings  ---> 
   Build Options  --->
        [ * ] Build BusyBox as a static binary (no shared libs)
Linux System Utilities  --->
   [ ] mount 
   [ ] umount

执行 make install 后, 会在 busybox 的源码目录地下创建一个 _install
的文件夹, 这个文件夹就是需要复制到文件系统里面的东西.
# make install

制作文件系统

首先创建一个虚拟盘, 并挂载到当前 tmp 目录下.

# cd ~/Develop
# dd if=/dev/zero of=initrd.img bs=1k count=8192
# mkfs.ext3 -F -v -m0 initrd.img
# mkdir tmp
# mount -o loop initrd.img  tmp

然后把编译好的 busybox 复制到挂在虚拟盘的目录里面.

# cp -dpRrf ~/Develop/Linux/busybox-1.19.3//_install/* tmp

创建一些必须的设备文件, 其实设备号几乎是通用的, 所以我直接把本机的设备文件
复制过来了.

# cp -dfrpa /dev/console tmp/dev
# cp -dfrpa /dev/tty* tmp/dev
# cp -dfrpa /dev/mem tmp/dev
# cp -dfrpa /dev/null tmp/dev
# cp -dfrpa /dev/random tmp/dev
# umount tmp

运行

# ~/Develop/QEMU/qemu-1.0/bin/qemu-system-x86_64 \
# -kernel ~/Develop/Linux/linux-2.6.34/arch/x86/boot/bzImage \
# -hda ~/Develop/initrd.img -m 2048 -append "root=/dev/sda init=/bin/sh"

在运行的时候碰到了 init 段错误的问题. 我不知道是不是静态编译导致的.
解决这个问题有两个办法, 或者从其他发行版复制一个静态的 busybox过来,
我试过, 没有问题. 或者把内核启动参数改为 init=/bin/sh 不让 kernel
去启动 init. 我选的是后者.

开始调试

-s 表示用默认的 1234 端口, 开启 gdb server

# ~/Develop/QEMU/qemu-1.0/bin/qemu-system-x86_64 \
# -s -S -kernel ~/Develop/Linux/linux-2.6.34/arch/x86/boot/bzImage \
# -hda ~/Develop/initrd.img -m 2048 -append "root=/dev/sda init=/bin/sh"

在我的 Emacs 里面, 使用 /tmp/gdb/bin/gdb –annotate=3 ~/Develop/Linux/linux-2.6.34/vmlinux
来启动, 进去以后, 设置断点, 然后 target remote localhost:1234 连接 gdb server

其它.

我 gdb 的启动指令是 /tmp/gdb/bin/gdb 而不是默认的 gdb, 因为默认的 gdb 在我的 x86
平台上调试的时候有一个小 bug, 根据邮件列表上的说法.
需要 hack 代码, 然后重新编译. 我打了一个小 patch.

--- gdb-7.3.1-orign/gdb/remote.c    2011-07-15 10:04:29.000000000 +0800
+++ gdb-7.3.1/gdb/remote.c  2011-12-27 18:37:34.319902796 +0800
@@ -5702,9 +5702,21 @@
   buf_len = strlen (rs->buf);
 
   /* Further sanity checks, with knowledge of the architecture.  */
+#if 0
   if (buf_len > 2 * rsa->sizeof_g_packet)
     error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
-
+#endif
+  if (buf_len > 2 * rsa->sizeof_g_packet) {
+     rsa->sizeof_g_packet = buf_len ;
+     for (i = 0; i < gdbarch_num_regs (gdbarch); i++) {
+         if (rsa->regs[i].pnum == -1)
+             continue;
+         if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
+             rsa->regs[i].in_g_packet = 0;
+         else 
+             rsa->regs[i].in_g_packet = 1;
+     }    
+  }
   /* Save the size of the packet sent to us by the target.  It is used
      as a heuristic when determining the max size of packets that the
      target can safely receive.  */

最后献上一张截图: (Emacs 又立功了)

Emacs_KGDB

分类: Kernel, kvm, linux, QEMU 标签: , , , ,

系统引导

2011年10月31日 1 条评论

本文基于 i386 架构分析.

计算机开机通电后 BIOS 将可启动设备(在 BIOS 里由用户设置, U盘, 硬盘, 光盘等)的第一个 扇区的 512 个字节读入内存绝对地址 0x7C00 处, 然后跳转到这个地方执行, 注意此时 CPU 处于 16 位地址的实模式.

远古时代的 Linux 的启动引导

在 boot 目录下共有三个文件 bootsect.S, setup.S, head.S 来做系统开始初始化的一些工作.

bootsect.S

bootsect.S 被编译链接后驻留在启动设备的第一个扇区的前 512 个字节处. 计算机加点启动后 BIOS 将会把 bootsect 加载到 内存 0x7C00 处并开始执行.

这个文件的主要目的是把 setup 和 system(Linux的真正代码)的代码加载到内存中.

bootsect 的执行流程如下:

  1. 用 movw 指令将 0x7C00 开始的 512 字节移动到 0x90000 处, 并跳转到 0x90000 开始执行.
  2. 从磁盘第二扇区开始读取4个扇区到 0x90200 处.
  3. 从磁盘的第扇区开始加载 0x30000(196KB) 的数据到 0x10000 处.
  4. 做完文件系统设备号的检查后, 通过 jmpi 0, SETUPSEG(0x90200) 跳转到 setup 处开始执行.

setup.S

setup 的主要目的是利用 BIOS 提供的中断服务程序(这些中断向量表存放在 0x000开始的位置, 所以 bootsect 加载 system 时候只能从 0x10000 开始) 从设备上提取内核运行所需的机器系统数据, 其中包括光标位置和显示页面, 硬盘参数表等数据, 把它们存放在 0x90000(覆盖 bootsect)开始的位置. 然后移动 system 到 0x000000, 然后设置各种参数, 为 linux 进入 保护模式做好准备.

setup 的执行流程如下:

  1. 将一些系统参数(内存, 硬盘等)存在 0x90000 处.
  2. 关闭中断(cli指令), 将 linux(system模块) 移动到 0x00000, 此时 BIOS 提供 的 16 位的中断机制已经没有了(被覆盖了).
  3. 打开 A20, 实现线性寻址.
  4. 对中断重新编程.
  5. 切换到保护模式.
  6. 跳转到(jmpi 0,8) , 8 = 0x00001000(表示0x00000的特权级, 全局描述符表)

head.S

注意, 此时开始采用 AT&T 的汇编语法,

head 的执行流程如下:

  1. 把相应的段寄存器设置为新的保护模式下的值.
  2. 重新设置中断描述符表(只是简单的初始化, 真正的中断以后再安装)
  3. 重新设置全局描述符表(段限长改为16M)
  4. 确定 A20 线是否开启, 是否含有协处理器.
  5. 以下几步非常关键, 把 main 需要的参数(三个空), L6, 和 main 压栈
  6. 然后设置分页: 开启分页功能. 在内存 0x0 处的 5K 存放一页页目录和四页页表 (此时这四页内核专用, 可寻址 16 M, 页表共用)

现代 Linux 的启动引导

Grub + kernel(): 未完待续

分类: Kernel, linux 标签: ,

坑爹的COFF格式

2011年8月4日 没有评论

最近在研究 TI 的 DSP, 有一件很坑爹的事情令我很恼火. 研究过 TI DSP 的人都知道, TI DSP 的开发主要分为三个部分, 一个是纯算法的开发, 一个是把算法整合成 DSP 可以执行, 并且可以和 ARM 的可执行文件通行的文件的开发, 最后一个是 ARM 程序的开发. 其中对于使用 TI DSP 的厂商来说, 他们开发的重点是 ARM 程序的开发, 对于第一和第二, 则由数量众多的第三方厂商来提供支持. 并且第一个(算法)一般是闭源的算法库.

坑爹的事情是这样的, 公司现在在基于 TI 最新的 DSP C674 系列的开发. 技术部门向 TI 申请到了一个 G.729 的编解码库. 我拿过来一看文件名后缀是 .a64P的, 明显是 C64 系列格式的 编解码库. 让公司的技术人员和他们交涉, 申请符合 C674 的 编解码库. TI 中国的分公司的工程师是这样说的, 674 和 64 系列的软件完全兼容.

好吧, 既然得到这样的答复, 我只好建立在相信对方的基础上去工作了. 对方提供的 Codec Engine的编译方式编译不过(TI 提供的 SDK叫EZSDK, 搞得人模鬼样的SDK, 用我最讨厌的javascript, java等等, 好像还有 cpp, 来自动生成代码), 于是我在花了几周的时间去了解底层 SDK 实现的细节. 直到把所有这些编译方式翻译成我所熟悉的 GNU Makefile + GCC的方式以后, 才发现, 对方给的库有问题, 用 EZSDK 根本编译不过, 然后看了一下编解码库的 readme 的时间(2008年), 再比较了一下 EZSDK 的发行时间(2011年6月). 心马上亮了半截. 然后去 TI的论坛 上去搜索 EZSDK 问题. 果然那边一堆骂娘的, 最后 TI 的工程师给了一句. 最新的 SDK(也就是 EZSDK)不支持 COFF 格式, 只支持 ELF格式, 而原来的SDK(DVSDK, 好吧我疯了)编译的库几乎都是 COFF 格式的, 包括我们申请到的 G.729 库. 我把 TI 的论坛上的答复发给了 TI 中国的工程师以后, 对方莫名其妙的给了我一句, 这个库不支持你们的平台, 新的库我们正在找人编译, 不要浪费时间在这个库上.

我 ……

下面是我这几天在 google 上找到的关于可执行文件格式的简单介绍:

a.out

UNIX 上最早的可执行文件格式, 特点是格式简单, 比较难实现共享库. 目前几乎被抛弃了.

COFF

COFF 比 a.out 是一种进步, 他在 a.out 的基础上增加了一个节段表, 比 a.out 更灵活. 但是默认情况下对动态连接和C++程序的支持仍然比较困难, 所以不同的操作系统做了不同的扩展, 比如 Microsoft 设计了名为 PE (Portable Executable) 的文件格式, 在 COFF 文件头部之上增加了一些专用头部.

ELF

这是现在 Linux 使用的文件格式, 一般我们用 file 指令查看一个可执行文件格式(比如 /bin/ls)的时候, 会有这样的信息:

# file `which ls`
/bin/ls: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, stripped

说明 ls 是一个 ELF 格式的可执行文件.

简单来说, ELF 格式更好的支持动态链接技术. 具有一下特点:

  • 链接库是内存位置无关的. 可以把动态库加载到内存的任何位置都没有影响. a.out 是内存地址有关的, 必须加载到相应的内存地址才行.
  • 动态加载. 比如一个程序需要用到某个动态库, 在没有调用到这个动态库的函数, 是不会把这个动态库加载到程序中的(也就是内存映射).
  • 动态解析. 要调用的函数被调用的时候, 才会去把这个函数在虚拟内存空间的起始地址解析出来, 再写到专门在调用模块中的储存地址内, 有点像 linux 的 COW(copy on write).

分类: dsp, linux 标签: , ,