程序分身2:参考busybox方式实现

背景

约半年前,曾经写过有关程序分身的文章,最近心血来潮翻看busybox的代码,发现原来实现的方式有点笨拙。如busybox名称所示,它将很多的程序都集成到一个程序(box)中,所以非常“busy”。笔者从事多年的嵌入式,发现大部分二进制程序都使用busybox,因为它能大大减少占用空间,即减少flash占用,亦即减少硬件成本。能达到这个目的,一方面利益于其架构,另一方面busybox使用了Linux的链接机制。

本文参照busybox的解析过程,并将其简化。

完整源码

main.c源码如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
* 参考busybox框架代码实现程序分身。
* 应用场合:很多业务处理可能需要不同的程序,但又有很多共同部分(如初始化)。
* 则可以使用同一套代码,但创建多个链接的方式实现,
* 综合考虑了代码组织维护和程序运行两方面。
* 具体示例:cgi业务程序
* 测试说明:
* gcc busybox_main.cpp
* cp a.out foo
* cp a.out hello
* 分别执行foo和hello和a.out(执行a.out没有输出信息)

*/
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
#include <string.h>

// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// 以下为不同业务处理模块程序,每个函数可以是独立的,也可以是有部分共用的初始化
// 但不能共享数据,因为对外,它们是单独的程序
int foo(int argc, char *argv[])
{
printf("foo\n");
return 0;
}

int bar(int argc, char *argv[])
{
printf("bar\n");
return 0;
}

int hello(int argc, char *argv[])
{
printf("hello\n");
for (int i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
// >>>>>>>>>>>>>>>>>>>>>>>>>>>> 业务模块函数 结束

#define ARRAY_SIZE(x) ((unsigned)(sizeof(x) / sizeof((x)[0])))

struct bb_applet
{
const char *name; // 程序名称
int (* applet_main)(int argc, char* argv[]); // 仿main函数
};

// 全局结构体数组,保存程序名称与对应的执行函数
static struct bb_applet g_applets[] =
{
{"foo", foo},
{"bar", bar},
{"hello", hello}
};

static int cmp_name(const void *a, const void *b)
{
const struct bb_applet *aa = (struct bb_applet *)a;
const struct bb_applet *bb = (struct bb_applet *)b;
return strcmp(aa->name, bb->name);
}

static int applet_name_compare(const void *name, const void *idx)
{
int i = (int)(ptrdiff_t)idx - 1;
return strcmp((const char*)name, g_applets[i].name);
}

int find_applet_by_name(int size, const char *name)
{
// 当数量达到一定时,使用二分查找,这样快速,否则直接比较即可
if (size > 8)
{
void *p;
p = bsearch(name, (void*)(ptrdiff_t)1, size, 1, applet_name_compare);

return (int)(ptrdiff_t)p - 1;
}
else
{
for (int i = 0; i < size; i++)
{
if (strcmp(name, g_applets[i].name) == 0)
return i;
}
return -1;
}
}

int main(int argc, char *argv[])
{
char* p = NULL;
char* applet_name = argv[0];
int size = ARRAY_SIZE(g_applets);

int applet_no = -1;

if ((p = strrchr (applet_name, '/')) != NULL) {
applet_name = p + 1;
}

// 测试代码
for (int i = 0; i < size; i++)
{
//printf("org name[%d]: %s\n", i, g_applets[i].name);

}
// 先进行快排
qsort(g_applets, size, sizeof(g_applets[0]), cmp_name);

// 测试代码
for (int i = 0; i < size; i++)
{
//printf("name[%d]: %s\n", i, g_applets[i].name);

}

// 再使用二分查找
applet_no = find_applet_by_name(size, applet_name);

// 如果找到,则执行
if (applet_no != -1)
g_applets[applet_no].applet_main(argc, argv);

return 0;
}

代码不难,首先处理输入程序名称的“./”,因为有时会使用“./a.out”的形式。接着使用快速排序,因为后面可能使用二查找算法查找程序名称。
使用上,按业务需求实现不同的模块函数,将名称与函数放置到bb_applet结构体的全局变量g_applets中。使用创建不同的链接文件(即bb_applet结构体中的name),指向生成的二进制文件。

编译

1
gcc main.c

执行:

1
2
3
4
5
6
7
8
9
$ ln -s a.out hello // 链接文件
$ ./hello a b c d e
hello
argv[0]: ./hello
argv[1]: a
argv[2]: b
argv[3]: c
argv[4]: d
argv[5]: e

实践应用、小结

在cgi程序中,web上不同的按钮都对应一个cgi程序,如果分开实现,一些诸如初始化的代码,就要在不同模块中出现。使用本文方法,可减少代码冗余,后期维护也方便。

李迟 2018.1.19 深夜