Linux Shell Tips

适用于桌面以及嵌入式 Linux 环境脚本编程。

1. 串口操作

配置串口模式(波特率 9600, 数据位 8 位,停止位 1 位,无校验):

1
stty -F /dev/ttyS0 9600 cs8 -cstopb -parenb

具体可以通过 man stty 来了解其他配置。

按行读取串口文本:

1
2
3
4
5
cat /dev/ttyS0 | while read line
do
    # do somehing with $line
    echo "$line"
done

2. 数值操作

整数变量累加可以参考以下示范:

1
2
3
4
5
count=0
echo $count     # => 1

let count+=1
echo $count     # => 2

计算平均数可以参考以下示范:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 计算数值列平均数
# 文件 data.txt 存放如下文本
# 31 41 59
# 26 53 58
# 97 93 23
# 

# 过滤空行
data=$(cat data.txt | sed '/^\s*$/d')
# 计算列和 以及 列平均数
echo "$data" | awk '{ c1+=$1; c2+=$2; c3+=$3 } END { printf "sum=%d, %d, %d\navg=%f, %f, %f\n", c1, c2, c3, c1/NR, c2/NR, c3/NR}'

# => sum=154, 187, 140
#    avg=51.333333, 62.333333, 46.666667

3. 字符串处理

字符串变量使用时尽量用双引号包含起来。

去除空白符(Strip Whitespace):

1
2
3
a=" ab cde fg "
a=$(echo "$a" | xargs)
echo "^$a$"  # => ^ab cde fg$

字符串判断:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
empty=""
ok="ok"
hello="hello"

# 判断字符串为空
if [ -z "$empty" ]; then
        echo "Empty String"
fi
# => Empty String

# 判断字符串非空
if [ -n "$hello" ]; then
        echo "Non-empty String"
fi
# => Non-empty String

# 判断字符串相等
if [ "$ok" = "ok" ]; then
        echo "OK"
else
        echo "FAIL"
fi
# => OK

# 判断字符串不等
if [ "$ok" != "ok" ]; then
        echo "Not Same"
else
        echo "Same"
fi
# => Same

apt源arm gcc交叉编译器注意事项

做嵌入式 Linux 开发时候都需要使用交叉编译工具,这些一般是方案厂商提供,但我们通常也可以使用那些打包好的通用交叉编译工具,比如 Debian 上 apt 也有打包好的 arm 交叉编译工具: gcc-arm-linux-gnueabi 和 gcc-arm-linux-gnueabihf,对应 c++ 版本以 g++ 开头,这两类主要区别在于 CPU 对于浮点的处理上,涉及到 armel 和 armhf 架构的区别,详情参照这里,你只需要选择对应开发板的架构工具即可。

由于是 Debian 自己维护更新的包,上述的两个交叉编译工具通常都会更新比较频繁,会支持最新的标准和特性,但是我们的开发板上系统通常都不是最新的,所以在使用时需要注意,否则会出现编译后的程序无法在开发板上运行。

最近在将 mruby 应用到嵌入式环境时就遇到这个问题。

问题和溯因

将静态链接了 mruby 库的测试程序 mirb 放入嵌入式环境上运行时,提示

1
./mirb: /lib/libc.so.6: version `GLIBC_2.16' not found (required by ./mirb)

嵌入式环境没有 2.16 版本以上的 glibc 库,但是理论上测试程序不需要这么高版本的 glibc 库。在嵌入式命令行上用 ldd (有些嵌入式环境可能没有附带这个工具)查看一下可执行文件信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#ldd -v mirb
./mirb: /lib/libc.so.6: version `GLIBC_2.16' not found (required by ./mirb)
        libm.so.6 => /lib/libm.so.6 (0x401ed000)
        libc.so.6 => /lib/libc.so.6 (0x40293000)
        /lib/ld-linux.so.3 (0x401c4000)

        Version information:
        ./mirb:
                libm.so.6 (GLIBC_2.4) => /lib/libm.so.6
                libc.so.6 (GLIBC_2.7) => /lib/libc.so.6
                libc.so.6 (GLIBC_2.16) => not found

                libc.so.6 (GLIBC_2.4) => /lib/libc.so.6
        /lib/libm.so.6:
                ld-linux.so.3 (GLIBC_PRIVATE) => /lib/ld-linux.so.3
                libc.so.6 (GLIBC_2.4) => /lib/libc.so.6
        /lib/libc.so.6:
                ld-linux.so.3 (GLIBC_2.4) => /lib/ld-linux.so.3
                ld-linux.so.3 (GLIBC_PRIVATE) => /lib/ld-linux.so.3

可执行程序 mirb 的确引用 2.16 版本的 glibc 库,那就继续查一下引用了 2.16 版本 glibc 库的哪些符号。

在开发主机(不需要在嵌入式主机中)上可以使用 objdump 工具查看可执行程序的符号表(参数 -t):

1
2
$ objdump -t mirb | grep 2.16
00000000       F *UND*	00000000              timespec_get@@GLIBC_2.16

可以看出是 timespec_get 这个函数符号需要 2.16 版本的 glibc 库,那么就得在代码中查一下哪个地方调用了这个函数。

在这里 可以看出 mruby 源码的 time.c 文件中使用了 timespec_get 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  tm = (struct mrb_time *)mrb_malloc(mrb, sizeof(*tm));
#if defined(TIME_UTC)
  {
    struct timespec ts;
    if (timespec_get(&ts, TIME_UTC) == 0) {
      mrb_free(mrb, tm);
      mrb_raise(mrb, E_RUNTIME_ERROR, "timespec_get() failed for unknown reasons");
    }
    tm->sec = ts.tv_sec;
    tm->usec = ts.tv_nsec / 1000;
  }
#elif defined(NO_GETTIMEOFDAY)

宏定义 TIME_UTC 控制着代码是否调用 timespec_get 函数。

通过搜索了解到,TIME_UTC 和 timespec_get 都是 C11 标准里定义的。由于在编译 mruby 时没有指定编译参数,交叉编译工具使用了默认参数导致 C11 标准也被支持,mruby 库也就使用了最新的 C11 标准,引用了新函数 timespec_get,产生了 glibc 2.16 版本的依赖。

解决方法

在知晓原因后,解决方法就简单了。就是在交叉编译 mruby 库时,去指定编译标准(c标准),修改 mruby 的编译配置文件 build_config.rb :

1
2
3
4
conf.cc do |cc|
    cc.command = "arm-linux-gnueabi-gcc"
    cc.flags << "-std=gnu99"
end

将编译 c 标准指定为 gnu99(-std=gnu99),这里也可以使用 c99 标准(-std=c99)。

然后重新编译测试程序,放到嵌入式环境上运行,OK。

结语

使用编译器尤其是交叉编译器时,最好要指定编译参数,确定编译标准,否则目标平台可能不支持,导致程序无法运行。在向第三方提供开发库而不是源码的情况下, 需要特别注意,尽量使用统一的编译参数标准,推荐 c99 或者 gnu99。

在遇到类似 /lib/libc.so.6: version GLIBC_X.XX not found 问题时,可以借助 ldd 和 objdump 工具来确定依赖的符号源,寻找解决方法。

CMake 项目模块化布局

在 C\C++ 项目中,通常会有不止一个可执行程序,或者需要将一些调用封装成动态或静态库,来实现项目的模块化,本文就讲解一种支持这种项目布局的方法。

CMake 支持子目录构建(add_subdirectory 指令),本文介绍的布局方法就是基于这个功能来实现。

项目

项目根目录编写一个 CMakeLists.txt,主要描述项目名称,也可以将一些项目级别的配置写入,来影响所有项目构建目标。比如编译的警告级别设置,如果要在项目级别上统一,可以写入,如果要针对不同构建目标来设置,可以留到构建目标 CMakeLists.txt 文件中写入。

项目的各个构建目标作为子目录存放在项目根目录下,每个子目录均包含相应的构建 CMakeLists.txt 文件。使用 add_subdirectory 指令将构建目标子目录添加到项目 CMakeLists.txt 中,构建目标间可以相互引用,只要在添加子目录到项目 CMakeLists.txt 时保持依赖顺序即可。

项目 CMakeLists.txt 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cmake_minimum_required(VERSION 2.6)

# 项目名称
set(PROJECT_NAME "TestApps")
project(${PROJECT_NAME})

# 统一构建目标输出目录
# 将所有构建目标输出目录定位在构建目录下,按类型分类子目录
#     静态库输出目录 => 构建目录/lib
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
#     动态库输出目录 => 构建目录/bin
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
#     可执行程序输出目录 => 构建目录/bin
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)

# ---------- 库 ----------
# staticlib 库
add_subdirectory(staticlib)
# 添加头文件搜索路径,这样其他构建目标可以引用其头文件
include_directories(staticlib)

# sharelib 库
add_subdirectory(sharelib)
# 添加头文件搜索路径,这样其他构建目标可以引用其头文件
include_directories(sharelib)

# ---------- 应用程序 ----------
# testapp 程序
add_subdirectory(testapp)

# 配置文件类
add_subdirectory(config)

项目 TestApps 中包含4个构建目标目录,2 个库 staticlib 和 sharelib,依赖库的 1 个应用程序 testapp,以及配置文件类目标 config。4 个目标的输出文件均统一到了构建目录下,按类型分类,这样便于构建结果的归档处理。

通常将构建目标子目录名与目标名一致,这样便于项目管理。

目标

每个构建目标对应在项目根目录下的各个子目录,每个子目录中都有一个 CMakeLists.txt 文件,记录着对应的构建配置。这些构建配置均会继承项目 CMakeLists.txt 文件中的配置。

以下针对示例项目,讲解各个子目录的 CMakeLists.txt ,用以理解如何进行具体的项目布局。

目标 staticlib(静态库编译示例):

1
2
3
4
5
6
7
8
9
# 编译选项
add_definitions("-std=gnu99")
add_definitions("-Wall")

set(SLIB "staticlib")
aux_source_directory("." SLIB_SRC)
add_library(${SLIB} STATIC ${SLIB_SRC})
# 依赖库
target_link_libraries(${SLIB} m)

add_library 指令添加一个库目标,参数 STATIC 表示编译成静态库,该目标链接时需要链接数学库。

目标 sharelib(动态库编译示例):

1
2
3
4
5
6
add_definitions("-std=gnu99")
add_definitions("-Wall")

set(DLIB "sharelib")
aux_source_directory("." DLIB_SRC)
add_library(${DLIB} SHARED ${DLIB_SRC})

与目标 staticlib 差不多,只是add_library 参数 SHARED 表示编译成动态库。

目标 testapp(执行程序编译示例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 编译选项
add_definitions("-std=gnu99")
add_definitions("-Wall")

set(APP "testapp")
aux_source_directory("." APP_SRC)
add_executable(${APP} ${APP_SRC})
target_link_libraries(${APP}
        staticlib
        sharelib)

add_executable 指令添加一个可执行目标,其依赖于静态库 staticlib 和动态库 sharelib。

目标 config(配置文件示例):

1
2
3
4
5
6
7
# 拷贝文件到输出目录,便于后续打包安装文件
file(COPY .
    DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
    # 忽略文件匹配模式
    PATTERN "CMakeLists.txt" EXCLUDE
    PATTERN "README.md" EXCLUDE
    )

config 目标是一个特殊目标,其不构建库或可执行程序,而是执行一个文件拷贝工作,在此示例中,将 config 目录下的文件(除 CMakeLists.txt 和 README.md)都拷贝到可执行输出目录中。这个用法一般用于拷贝程序的默认配置文件。这样做的好处在于可以将默认配置文件纳入版本管理,也方便打包程序以及测试部署,既能将代码与输出分离,又能实现自动化整合。

结语

CMake 是一个很便利的跨平台构建工具,使用模块化布局方法可以更契合多人共享开发模式,促进项目复用代码,方便上下游库协同管理。如果与 Git 的 submodule 配合,就更能体现出模块化。