在工作中经常碰到需要写一些多进程/多线程的测试程序,用来测试接口的性能。本文将会从零开始一点点增加代码,最终完成一个简易的多进程测试程序编写。该程序支持实时打印测试进结果和最终测试结果的统计。
同时,本文还涵盖了以下知识点,可以作为学习参考:
- 使用
getopt_long()
处理命令行选项和参数 - 使用
fork()
和wait()
处理多进程 - 使用
sigaction()
配合alarm()
处理定时信号SIGALRM
- 使用
shmget()
、shmat()
、shmdt()
、shmctl()
等通过共享内存进行进程间通信 - 使用
sigaction()
捕获SIGINT
和SIGQUIT
信号,在程序终止前做共享内存清理工作
本博客已经迁移至CatBro's Blog,那里是我自己搭建的个人博客,页面效果比这边更好,支持站内搜索,评论回复还支持邮件提醒,欢迎关注。这边只会在有时间的时候不定期搬运一下。
本文源码已开源Github
选项和参数的处理
为了使测试程序更高的可用性,我们getopt
来处理选项和参数。
#include <stdio.h> // printf
#include <getopt.h> // getopt_long
#include <stdlib.h> // strtol, abort
#include <limits.h> // LONG_MIN, LONG_MAX
void ShowHelpInfo(char *name) {
printf("Usage: %s [options]\n\n", name);
printf(" Options:\n");
printf(" -p/--proc Number of processes (default: 1)\n");
printf(" -d/--duration Duration of test (unit: s, default: 10)\n");
printf(" -h/--help Show the help info\n");
printf("\n");
printf(" Example:\n");
printf(" %s -p 4 -d 30\n", name);
printf("\n");
}
int main(int argc, char *argv[]) {
int c = 0;
int option_index = 0;
long procs = 1;
long duration = 10;
/**
* 定义命令行参数列表,option结构的含义如下(详见 man 3 getopt):
* struct option {
* const char *name; // 参数的完整名称,对应命令中的 --xxx
* int has_arg; // 该参数是否带有一个值,如 –-config xxx.conf
* int *flag; // 一般设置为NULL
* int val; // 解析到该参数后getopt_long函数的返回值,
* // 为了方便维护,一般对应getopt_long调用时第三个参数
* };
*/
static struct option arg_options[] =
{
{"proc", 1, NULL, 'p'},
{"duration", 1, NULL, 'd'},
{"help", 0, NULL, 'h'},
{NULL, 0, NULL, 0}
};
/**
* 注意:传递给getopt_long的第三个参数对应了命令行参数的缩写形式,如-h等,
* 如果字符后面带有冒号":",则说明该参数后跟一个值,如-c xxxxxx
* 如果开头有冒号":",则当一个选项缺少参数时,返回":",否则,返回"?"
*/
while ((c = getopt_long(argc, argv, ":p:d:h", arg_options, &option_index)
) != -1) {
switch (c) {
case 'h':
ShowHelpInfo(argv[0]);
//fprintf(stdout,"option is -%c, optarv is %s\n", c, optarg);
return 0;
case 'p':
procs = strtol(optarg, NULL, 0);
if (procs == LONG_MIN || procs == LONG_MAX) {
fprintf(stderr, "The number of processes (%s) is overflow\n\n",
optarg);
ShowHelpInfo(argv[0]);
return -1;
}
else if (procs <= 0) {
fprintf(stderr, "The number of processes must be > 0\n\n");
ShowHelpInfo(argv[0]);
return -1;
}
break;
case 'd':
duration = strtol(optarg, NULL, 0);
if (duration == LONG_MIN || duration == LONG_MAX) {
fprintf(stderr, "The duration of test (%s) is overflow\n\n",
optarg);
ShowHelpInfo(argv[0]);
return -1;
}
else if (procs <= 0) {
fprintf(stderr, "The duration of test must be > 0\n\n");
ShowHelpInfo(argv[0]);
return -1;
}
break;
case '?':
fprintf (stderr, "Unknown option -%c\n\n", optopt);
ShowHelpInfo(argv[0]);
return -1;
case ':':
fprintf (stderr, "Option -%c requires an argument\n\n", optopt);
ShowHelpInfo(argv[0]);
return -1;
default:
abort();
}
}
printf("processes: %ld\n", procs);
printf("duration: %lds\n", duration);
printf("\n-----------------------------Start Testing----------------------"
"--------\n\n");
printf("Hello world\n");
return 0;
注意:传递给getopt_long的第三个参数对应了命令行参数的缩写形式,如-h, -v, -c等。
如果字符后面带有冒号":",则说明该参数后跟一个值,如-c xxxxxx
如果开头有冒号":",则当一个选项缺少参数时,返回":",否则,返回"?"
效果如下:
^_^$ make
gcc "-g" -c multi-process.c -o multi-process.o
gcc -o test multi-process.o
^_^$ ./test
processes: 1
duration: 10s
-----------------------------Start Testing------------------------------
Hello world
选项或参数错误时:
^_^$ ./test -p
Option -p requires an argument
Usage: ./test [options]
Options:
-p/--proc Number of processes (default: 1)
-d/--duration Duration of test (unit: s, default: 10)
-h/--help Show the help info
Example:
./test -p 4 -d 30
增加多进程的支持
主进程fork
出n个子进程后wait
子进程,子进程则通过sigaction
和alarm
设置一个定时器,然后进行业务测试。
为了简洁,已经把选项参数处理的部分独立出去了。
#include <stdio.h> // printf, fprintf
#include <sys/wait.h> // wait
#include <sys/types.h> // getpid, wait
#include <signal.h> // sigaction, SIGLARM
#include <limits.h> // LONG_MIN, LONG_MAX, ULLONG_MAX
#include <unistd.h> // getpid
#include <string.h> // memset
#include "multi-process.h"
int isStop = 0; // 用于标记测试终止
typedef struct param_st { // 自定义测试参数
long index;
} Param;
void handle_signal_child(int sigNum)
{
if (sigNum == SIGALRM) {
isStop = 1;
}
}
/* 实际业务测试函数 */
void doTest(void *param) {
unsigned long long i = 0;
Param *pa = (Param *)param;
for(; i < ULLONG_MAX && !isStop; ++i) {
/* DO YOUR WORK */
/* DO YOUR WORK */
}
printf("process [pid = %6u] result: %llu\n", getpid(), i);
}
int main(int argc, char *argv[]) {
int rv = 0;
long i = 0;
int proc_index = 0;
Options opt;
int isParent = 1;
int wstatus = 0;
pid_t pid = 0;
struct sigaction act_child;
rv = process_options(argc, argv, &opt);
if (rv) {
return -1;
}
printf("\n-----------------------------Start Testing----------------------"
"--------\n\n");
/* COMMON INIT */
/* COMMON INIT */
while(isParent && i < opt.procs) {
pid = fork();
if(pid == -1) { /* error */
fprintf(stderr, "fork failed %d\n", pid);
return -1;
}
else if(pid == 0) { /* child */
isParent = 0;
proc_index = i; // 记录进程索引
}
else { /* parent */
}
++i;
}
if(isParent) {
/* PARENT INIT */
/* PARENT INIT */
for(i =0 ; i < opt.procs; ++i) {
pid = wait(&wstatus); // 等待子进程结束
printf("process [pid = %6d] exit\n", pid);
}
}
else {
/* CHILD INIT */
Param param;
memset(¶m, 0, sizeof(Param));
param.index = proc_index;
/* CHILD INIT */
act_child.sa_handler = handle_signal_child;
sigemptyset(&act_child.sa_mask);
act_child.sa_flags = SA_RESETHAND;
/* 用于测试时间到时,通知子进程结束测试 */
rv = sigaction(SIGALRM, &act_child, NULL);
if (rv) {
fprintf(stderr, "sigaction() failed\n");
return -1;
}
//signal(SIGALRM, handle_signal_child);
alarm(opt.duration); // 设置测试时长
doTest(¶m);
return 0; /* child finished work */
}
printf("Hello World!\n");
return 0;
}
效果如下:
^_^$ ./test -p 4 -d 2
processes: 4
duration: 2s
-----------------------------Start Testing------------------------------
process [pid = 11942] result: 446930553
process [pid = 11942] exit
process [pid = 11939] result: 434385097
process [pid = 11939] exit
process [pid = 11940] result: 442246977
process [pid = 11940] exit
process [pid = 11941] result: 442418811
process [pid = 11941] exit
这样已经可以实现简单的多进程测试,简单起见,示例代码里只是简单地进行了计数操作。读者如果想要进行自己特定的测试,只要在Param中增加需要的测试参数,接着在/* CHILD INIT */
处进行参数初始化,然后在/* DO YOUR WORK */
处添加实际的测试逻辑即可。
增加实时的结果统计及最终的结果汇总
为了使测试程序更加人性化,使其可以实时统计测试结果,结束时自动计算总的结果。这就需要引入父子进程间通信,我们选用共享内存的方式来实现。为了避免进程间同步对测试带来的影响,在共享内存中为每个子进程开辟了一个空间,每个子进程根据索引在自己的空间里写数据,由父进程进行结果的汇总。
#include <stdio.h> // printf, fprintf
#include <sys/wait.h> // wait
#include <sys/types.h> // getpid, wait
#include <sys/ipc.h> // shmget, shmat, shmctl, shmdt
#include <sys/shm.h> // shmget, shmat, shmctl, shmdt
#include <signal.h> // sigaction, SIGLARM
#include <limits.h> // LONG_MIN, LONG_MAX, ULLONG_MAX
#include <errno.h> // errno
#include <unistd.h> // getpid
#include <string.h> // memset
#include "multi-process.h"
typedef struct param_st { // 自定义测试参数
long index;
} Param;
typedef struct result_st { // 自定义测试结果
unsigned long long count;
} Result;
int isStop = 0; // 用于标记测试终止
Options opt; // 命令行选项
int shmid; // 共享内存id
Result *shm = NULL; // 共享内存地址,用于存放测试结果
Result res_total;
Result res_last;
void handle_signal_child(int sigNum)
{
if (sigNum == SIGALRM) {
isStop = 1;
}
}
void handle_signal_parent(int sigNum)
{
if (sigNum == SIGALRM) {
/* DO REAL-TIME STATISTICS */
memset(&res_total, 0, sizeof(Result));
for (long i = 0; i < opt.procs; ++i) {
res_total.count += shm[i].count;
}
fprintf(stderr, "total count %12llu, average %12.0lf/s\n",
res_total.count, (res_total.count - res_last.count)
/ (double)opt.interval);
memcpy(&res_last, &res_total, sizeof(Result));
/* DO REAL-TIME STATISTICS */
alarm(opt.interval);
}
}
/* 实际业务测试函数 */
void doTest(void *param) {
unsigned long long i = 0;
Param *pa = (Param *)param;
for (; i < ULLONG_MAX && !isStop; ++i) {
/* DO YOUR WORK */
++shm[pa->index].count;
/* DO YOUR WORK */
}
}
int main(int argc, char *argv[]) {
int rv = 0;
long i = 0;
int proc_index = 0;
int isParent = 1;
int wstatus = 0;
pid_t pid = 0;
struct sigaction act_child;
struct sigaction act_parent;
rv = process_options(argc, argv, &opt);
if (rv) {
return -1;
}
fprintf(stderr, "\n-----------------------------Start Testing-------------"
"-----------------\n\n");
/* COMMON INIT */
shmid = shmget(IPC_PRIVATE, sizeof(sizeof(Result) * opt.procs), 0666);
if (-1 == shmid) {
fprintf(stderr, "shmget() failed\n");
return -1;
}
fprintf(stderr, "shmid = %d\n", shmid);
shm = (Result*)shmat(shmid, 0, 0);
if ((void *) -1 == shm) {
fprintf(stderr, "shmat() failed\n");
return -1;
}
memset(shm, 0, sizeof(sizeof(Result) * opt.procs));
/* COMMON INIT */
while(isParent && i < opt.procs) {
pid = fork();
if(pid == -1) { /* error */
fprintf(stderr, "fork failed %d\n", pid);
return -1;
}
else if(pid == 0) { /* child */
isParent = 0;
proc_index = i; // 记录进程索引
}
else { /* parent */
}
++i;
}
if(isParent) {
/* PARENT INIT */
memset(&act_parent, 0, sizeof(act_parent));
act_parent.sa_handler = handle_signal_parent;
/* 使wait被中断时可以自动恢复 */
act_parent.sa_flags = SA_RESTART;
rv = sigaction(SIGALRM, &act_parent, NULL); // 用于定时统计结果
//signal(SIGALRM, handle_signal_parent);
if (rv) {
fprintf(stderr, "sigaction() failed\n");
return -1;
}
memset(&res_last, 0, sizeof(Result));
alarm(opt.interval);
/* PARENT INIT */
/* DO FINAL STATISTICS */
Result final;
memset(&final, 0, sizeof(Result));
for(i =0 ; i < opt.procs; ++i) {
pid = wait(&wstatus); // 等待子进程结束
alarm(0); // 终止定时器
if(pid == -1) {
fprintf(stderr, "wait() failed, errno=%d\n", errno);
return -1;
}
fprintf(stderr, "process [pid = %6d] exit\n", pid);
fprintf(stderr, "process [pid = %6u] count %12llu in %lus, "
"average %12.0lf/s\n", pid, shm[i].count, opt.duration,
shm[i].count / (double)opt.duration);
final.count += shm[i].count;
}
fprintf(stderr, "total count %12llu in %lus, average %12.0lf/s\n",
final.count, opt.duration, final.count / (double)opt.duration);
/* DO FINAL STATISTICS */
shmdt((void*)shm);
/* 子进程退出之后自动detach了, 所以这里不需要通过IPC_STAT进行判断 */
shmctl(shmid, IPC_RMID, 0);
}
else {
/* CHILD INIT */
Param param;
memset(¶m, 0, sizeof(Param));
param.index = proc_index;
/* CHILD INIT */
act_child.sa_handler = handle_signal_child;
sigemptyset(&act_child.sa_mask);
//sigaddset(&act_child.sa_mask, SIGQUIT);
//sigaddset(&act_child.sa_mask, SIGTERM);
act_child.sa_flags = SA_RESETHAND;
/* 用于测试时间到时,通知子进程结束测试 */
rv = sigaction(SIGALRM, &act_child, NULL);
if (rv) {
fprintf(stderr, "sigaction() failed\n");
return -1;
}
//signal(SIGALRM, handle_signal_child);
alarm(opt.duration); // 设置测试时长
doTest(¶m);
return 0; /* child finished work */
}
return 0;
}
测试效果如下:
^_^$ ./test -p 4 -d 8 -i 1
processes: 4
duration: 8s
interval: 1s
-----------------------------Start Testing------------------------------
shmid = 2654220
total count 344235932, average 344235932/s
total count 679573681, average 335337749/s
total count 1026283924, average 346710243/s
total count 1368302354, average 342018430/s
total count 1708471662, average 340169308/s
total count 2057211138, average 348739476/s
total count 2398403059, average 341191921/s
process [pid = 25124] exit
process [pid = 25124] count 688504473 in 8s, average 86063059/s
process [pid = 25123] exit
process [pid = 25123] count 682379115 in 8s, average 85297389/s
process [pid = 25125] exit
process [pid = 25125] count 682467102 in 8s, average 85308388/s
process [pid = 25126] exit
process [pid = 25126] count 688159459 in 8s, average 86019932/s
total count 2741510149 in 8s, average 342688769/s
这里需要特别提一下wait()
和sigaction()
系统调用,默认情况下wait()
会阻塞直到有任意一个子进程改变了其状态,或者有一个信号处理函数中断了wait()
调用。所以我们程序中的wait()
调用就会被自己的SIGALRM
信号中断,返回-1
同时errno
为EINTR
。这样我们就需要在wait()
外面加一层循环来处理wait()
被信号中断的情况。
通过在sigaction()
时增加SA_RESTART
标志,被中断的系统调用可以自动重开,也就省去了那个外层循环。另外,signal()
封装了sigaction()
,它里面默认就是设置了SA_RESTART
,不过除非你有确切的理由,不然不建议使用signal()
了。
增加SIGINT和SIGQUIT信号捕获
截止目前,我们已经完成了多进程的测试及结果统计。但其实还有一个潜在的问题,在实际测试中,我们经常会在测试还没完成时就手动^C
终止程序执行。这样我们在程序中申请的共享内存就会得不到释放,造成内存泄漏。所以需要增加对SIGINT
和SIGQUIT
信号的处理函数,在里面做清理工作,释放共享内存。
如果已经不小心造成了共享内存的泄漏,可以通过如下命令手动进行删除。ipcrm shm <id>
,如果是显式指定key的话也可以通过ipcrm -M <key>
来进行删除。
今天,突然想到了,其实有一种更加简单的方法,即在shmat()
之后立即进行shmctl(shmid, IPC_RMID, 0);
。这样不仅简单,而且中间的空窗期也更短,cool!这样我们再也不用担心,^C
造成内存泄漏了哈。
@@ -88,12 +88,14 @@ int main(int argc, char *argv[]) {
fprintf(stderr, "shmget() failed\n");
return -1;
}
fprintf(stderr, "shmid = %d\n", shmid);
shm = (Result*)shmat(shmid, 0, 0);
if ((void *) -1 == shm) {
fprintf(stderr, "shmat() failed\n");
return -1;
}
+ /* 这里直接进行IPC_RMID操作,进程退出后会自动detach了, 从而释放共享内存 */
+ shmctl(shmid, IPC_RMID, 0);
memset(shm, 0, sizeof(sizeof(Result) * opt.procs));
/* COMMON INIT */
@@ -156,10 +135,6 @@ int main(int argc, char *argv[]) {
fprintf(stderr, "total count %12llu in %lus, average %12.0lf/s\n",
final.count, opt.duration, final.count / (double)opt.duration);
/* DO FINAL STATISTICS */
-
- shmdt((void*)shm);
- /* 子进程退出之后自动detach了, 所以这里不需要通过IPC_STAT进行判断 */
- shmctl(shmid, IPC_RMID, 0);
}
else {
/* CHILD INIT */
封装错误判断函数
为了使代码看起来更加简洁,避免每个函数调用后面跟着一个if(){}
判断块,我们对错误判断及日志打印函数进行了一个简单的封装。封装的函数如下:
其中mylog()
单纯打印日志,fail()
打印日志后退出进程,fail_if()
先判断条件,如果成立打印日志退出,fail_clean_if()
也是先判断条件,条件成立则打印日志,执行传入的清理函数。然后退出进程。
#include "common.h"
#include <stdio.h> // stderr
#include <stdarg.h> // va_start, vfprintf, va_end
#include <stdlib.h> // exit
void fail_if(bool condition, const char *fmt, ...) {
if (condition) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
exit(1);
}
}
void fail(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
exit(1);
}
void mylog(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
}
void fail_clean_if(bool condition, cleanup clean, void *param, const char *fmt, ...) {
if (condition) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
clean(param);
exit(1);
}
}
添加实际测试用例
接下来我们来添加实际有意义的测试用例,这里以OpenSSL引擎的性能测试为例来进行说明。为了我们的代码更清晰,已经将具体测试相关的代码独立为一个源文件。common.c
封装错误函数,opt.c
处理命令行选项,work.c
处理具体测试,multi-process.c
则负责测试的主控。完整的代码如下:
common.h
#ifndef HEADER_COMMON_H
#define HEADER_COMMON_H
#include <stdbool.h> // bool, true, false
typedef void (*cleanup) (void *);
void fail_if(bool condition, const char *fmt, ...);
void fail(const char *fmt, ...);
void mylog(const char *fmt, ...);
void fail_clean_if(bool condition, cleanup clean, void *param, const char *fmt, ...);
#endif /* HEADER_COMMON_H */
common.c
#include "common.h"
#include <stdio.h> // stderr
#include <stdarg.h> // va_start, vfprintf, va_end
#include <stdlib.h> // exit
void fail_if(bool condition, const char *fmt, ...) {
if (condition) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
exit(1);
}
}
void fail(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
exit(1);
}
void mylog(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
}
void fail_clean_if(bool condition, cleanup clean, void *param, const char *fmt, ...) {
if (condition) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
clean(param);
exit(1);
}
}
opt.h
#ifndef HEADER_OPT_H
#define HEADER_OPT_H
typedef enum test{
hash, sign, verify, enc, dec
} Test;
typedef struct options_st {
long procs; // 进程数
long duration; // 测试时间
long interval; // 统计间隔
Test test; // 测试类型
long len; // 摘要原文长度
const char *key; // 密钥文件路径
const char *cert; // 证书文件路径
const char *loglevel; // 日志等级
} Options;
/* 处理参数 */
int process_options(int argc, char *argv[], Options *opt);
#endif /* HEADER_OPT_H */
opt.c
#include "opt.h"
#include <limits.h> // LONG_MIN, LONG_MAX, ULLONG_MAX
#include <string.h> // memset
#include <stdlib.h> // strtol, abort
#include <getopt.h> // geropt_long
#include "common.h" // mylog
static void ShowHelpInfo(char *name) {
mylog("Usage: %s [options]\n\n", name);
mylog(" Options:\n");
mylog(" -p/--proc Number of processes (default: 1)\n");
mylog(" -d/--duration Duration of test (unit: s, default: 10)\n");
mylog(" -i/--interval Interval of statisics (unit: s, default: 1)\n");
mylog(" -t/--test Test case (hash|sign|verify|enc|dec, default: hash)\n");
mylog(" -l/--len Hash data len (unit: byte, default: 1024)\n");
mylog(" -k/--key PEM Key file path (default: ./key.pem)\n");
mylog(" -c/--cert PEM Cert file path (default: ./cert.pem)\n");
mylog(" -o/--loglevel Engine log level (0-9, default: 0)\n");
mylog(" -h/--help Show the help info\n");
mylog("\n");
mylog(" Example:\n");
mylog(" %s -p 1 -d 30 -i 1 -t sign -k key.pem -c cert.pem\n", name);
mylog("\n");
}
/* 处理参数 */
int process_options(int argc, char *argv[], Options *opt) {
int c = 0;
int option_index = 0;
long procs = 1;
long duration = 10;
long interval = 1;
Test test = hash;
long len = 1024;
const char *key = "./key.pem";
const char *cert = "./cert.pem";
const char *loglevel = "0";
/**
* 定义命令行参数列表,option结构的含义如下(详见 man 3 getopt):
* struct option {
* const char *name; // 参数的完整名称,对应命令中的 --xxx
* int has_arg; // 该参数是否带有一个值,如 –config xxx.conf
* int *flag; // 一般设置为NULL
* int val; // 解析到该参数后getopt_long函数的返回值,
* // 为了方便维护,一般对应getopt_long调用时第三个参数
* };
*/
static struct option arg_options[] =
{
{"proc", 1, NULL, 'p'},
{"duration", 1, NULL, 'd'},
{"interval", 1, NULL, 'i'},
{"test", 1, NULL, 't'},
{"len", 1, NULL, 'l'},
{"key", 1, NULL, 'k'},
{"cert", 1, NULL, 'c'},
{"log", 1, NULL, 'g'},
{"help", 0, NULL, 'h'},
{NULL, 0, NULL, 0}
};
/**
* 注意:传递给getopt_long的第三个参数对应了命令行参数的缩写形式,如-h等,
* 如果字符后面带有冒号":",则说明该参数后跟一个值,如-c xxxxxx
* 如果开头有冒号":",则当一个选项缺少参数时,返回":",否则,返回"?"
*/
while ((c = getopt_long(argc, argv, ":p:d:i:t:l:k:c:g:h", arg_options, &option_index)
) != -1) {
switch (c) {
case 'h':
ShowHelpInfo(argv[0]);
//fprintf(stderr,"option is -%c, optarv is %s\n", c, optarg);
exit(0);
case 'p':
procs = strtol(optarg, NULL, 0);
if (procs == LONG_MIN || procs == LONG_MAX) {
mylog("The number of processes (%s) is overflow\n\n", optarg);
ShowHelpInfo(argv[0]);
return -1;
}
else if (procs <= 0) {
mylog("The number of processes must be > 0\n\n");
ShowHelpInfo(argv[0]);
return -1;
}
break;
case 'd':
duration = strtol(optarg, NULL, 0);
if (duration == LONG_MIN || duration == LONG_MAX) {
mylog("The duration of test (%s) is overflow\n\n", optarg);
ShowHelpInfo(argv[0]);
return -1;
}
else if (duration <= 0) {
mylog("The duration of test must be > 0\n\n");
ShowHelpInfo(argv[0]);
return -1;
}
break;
case 'i':
interval = strtol(optarg, NULL, 0);
if (interval == LONG_MIN || interval == LONG_MAX) {
mylog("The interval of statistics (%s) is overflow\n\n",
optarg);
ShowHelpInfo(argv[0]);
return -1;
}
else if (interval <= 0) {
mylog("The interval of statistics must be > 0\n\n");
ShowHelpInfo(argv[0]);
return -1;
}
break;
case 't':
if(!strcasecmp("hash", optarg)) {
test = hash;
}
else if(!strcasecmp("sign", optarg)) {
test = sign;
}
else if(!strcasecmp("verify", optarg)) {
test = verify;
}
else if(!strcasecmp("enc", optarg)) {
test = enc;
}
else if(!strcasecmp("dec", optarg)) {
test = dec;
}
else {
mylog("Unknown test case type\n\n");
ShowHelpInfo(argv[0]);
return -1;
}
break;
case 'l':
len = strtol(optarg, NULL, 0);
if (len == LONG_MIN || len == LONG_MAX) {
mylog("The len of hash data (%s) is overflow\n\n",
optarg);
ShowHelpInfo(argv[0]);
return -1;
}
else if (len <= 0) {
mylog("The len of hash data must be > 0\n\n");
ShowHelpInfo(argv[0]);
return -1;
}
break;
case 'k':
key = optarg;
break;
case 'c':
cert = optarg;
break;
case 'g':
loglevel = optarg;
break;
case '?':
mylog("Unknown option -%c\n\n", optopt);
ShowHelpInfo(argv[0]);
return -1;
case ':':
mylog("Option -%c requires an argument\n\n", optopt);
ShowHelpInfo(argv[0]);
return -1;
default:
exit(1);
}
}
mylog("processes: %ld\n", procs);
mylog("duration: %lds\n", duration);
mylog("interval: %lds\n", interval);
mylog("test: #%d\n", test);
mylog("len: %ld bytes\n", len);
mylog("key: %s\n", key);
mylog("cert: %s\n", cert);
mylog("loglevel: %s\n", loglevel);
memset(opt, 0, sizeof(Options));
opt->procs = procs;
opt->duration = duration;
opt->interval = interval;
opt->test = test;
opt->len = len;
opt->key = key;
opt->cert = cert;
opt->loglevel = loglevel;
return 0;
}
work.h
#ifndef HEADER_WORK_H
#define HEADER_WORK_H
#include <openssl/evp.h>
#include <openssl/bio.h>
#include "opt.h"
/* 自定义测试参数 */
typedef struct hash_param_st {
long index;
unsigned char *data;
unsigned int data_len;
unsigned char md[128];
unsigned int md_len;
EVP_MD_CTX *ctx;
} HashParam;
typedef struct sign_param_st {
long index;
unsigned char data[48];
size_t data_len;
unsigned char sig[256];
size_t sig_len;
EVP_PKEY_CTX *ctx;
BIO *in;
} SignParam;
typedef SignParam VerifyParam;
typedef struct enc_param_st {
long index;
unsigned char data[48];
size_t data_len;
unsigned char enc[256];
size_t enc_len;
EVP_PKEY_CTX *ctx;
BIO *in;
} EncParam;
typedef EncParam DecParam;
/* 自定义测试结果 */
typedef struct result_st {
unsigned long long count;
} Result;
typedef void (*init_fn) (Options *opt, void **param, long proc_index);
typedef void (*work_fn) (void *param);
typedef void (*clean_fn) (void *param);
extern init_fn test_init;
extern work_fn test_work;
extern clean_fn test_clean;
void global_init(Options *opt);
void global_clean();
#endif /* HEADER_WORK_H */
work.c
#include "work.h"
#include <string.h>
#include <openssl/engine.h>
#include <openssl/pem.h>
#include "common.h"
const char* so_path = "/usr/local/ssl/lib/myengine.so";
init_fn test_init = NULL;
work_fn test_work = NULL;
clean_fn test_clean = NULL;
static void hash_init(Options *opt, void **param, long proc_index);
static void hash_work(void *param);
static void hash_clean(void *param);
static void sign_init(Options *opt, void **param, long proc_index);
static void sign_work(void *param);
static void sign_clean(void *param);
static void verify_init(Options *opt, void **param, long proc_index);
static void verify_work(void *param);
static void verify_clean(void *param);
static void encrypt_init(Options *opt, void **param, long proc_index);
static void encrypt_work(void *param);
static void encrypt_clean(void *param);
static void decrypt_init(Options *opt, void **param, long proc_index);
static void decrypt_work(void *param);
static void decrypt_clean(void *param);
void global_init(Options *opt) {
ENGINE *e = NULL;
if (opt->test == hash) {
test_init = hash_init;
test_work = hash_work;
test_clean = hash_clean;
}
else if (opt->test == sign) {
test_init = sign_init;
test_work = sign_work;
test_clean = sign_clean;
}
else if (opt->test == verify) {
test_init = verify_init;
test_work = verify_work;
test_clean = verify_clean;
}
else if (opt->test == enc) {
test_init = encrypt_init;
test_work = encrypt_work;
test_clean = encrypt_clean;
}
else if (opt->test == dec) {
test_init = decrypt_init;
test_work = decrypt_work;
test_clean = decrypt_clean;
}
OpenSSL_add_all_algorithms();
/* ENGINE INIT */
ENGINE_load_dynamic();
if (!(e = ENGINE_by_id("dynamic"))) {
fail("ENGINE_by_id(\"dynamic\") fail\n");
}
if (!ENGINE_ctrl_cmd_string(e, "SO_PATH", so_path, 0)) {
fail("ENGINE_ctrl_cmd_string(\"SO_PATH\") fail, so_path = %s\n",
so_path);
}
if (!ENGINE_ctrl_cmd_string(e, "LIST_ADD", "1", 0)) {
fail("ENGINE_ctrl_cmd_string(\"LIST_ADD\") fail\n");
}
if (!ENGINE_ctrl_cmd_string(e, "LOAD", NULL, 0)) {
fail("ENGINE_ctrl_cmd_string(\"LOAD\") fail\n");
}
if (!ENGINE_init(e)) {
fail("ENGINE_init() fail\n");
}
if (!ENGINE_ctrl_cmd_string( e, "ENGINE_SET_LOGLEVEL", opt->loglevel, 0)) {
fail("ENGINE_ctrl_cmd_string(\"ENGINE_SET_LOGLEVEL\") fail\n");
}
if (!ENGINE_set_default(e, ENGINE_METHOD_ALL)) {
fail("ENGINE_set_default() fail\n");
}
ENGINE_free(e);
/* ENGINE INIT */
}
void global_clean() {
EVP_cleanup();
}
static void hash_init(Options *opt, void **param, long proc_index) {
HashParam *p;
p = OPENSSL_malloc(sizeof(HashParam));
fail_if(!p, "OPENSSL_malloc() fail\n");
memset(p, 0, sizeof(HashParam));
p->ctx = EVP_MD_CTX_create();
fail_clean_if(!p->ctx, hash_clean, (void *)p, "EVP_MD_CTX_create() fail\n");
EVP_MD_CTX_init(p->ctx);
p->data = OPENSSL_malloc(opt->len);
fail_clean_if(!p->data, hash_clean, (void *)p, "OPENSSL_malloc() fail\n");
fail_clean_if(RAND_bytes(p->data, sizeof(opt->len)) <= 0, hash_clean, (void *)p,
"RAND_bytes() fail\n");
p->data_len = opt->len;
p->index = proc_index;
*param = p;
}
static void hash_work(void *param) {
HashParam *p = (HashParam *)param;
const EVP_MD *md = EVP_get_digestbyname("SHASH");
fail_clean_if(!md, hash_clean, param,
"EVP_get_digestbyname(\"SHASH\") fail\n");
fail_clean_if(!EVP_DigestInit_ex(p->ctx, md, NULL),
hash_clean, param, "EVP_DigestInit_ex() fail\n");
fail_clean_if(!EVP_DigestUpdate(p->ctx, p->data, p->data_len),
hash_clean, param, "EVP_DigestUpdate() fail\n");
fail_clean_if(!EVP_DigestFinal_ex(p->ctx, p->md, &p->md_len),
hash_clean, param, "EVP_DigestFinal_ex() fail\n");
}
static void hash_clean(void *param) {
HashParam *p = (HashParam *)param;
if (p->data)
OPENSSL_free(p->data);
if (p->ctx)
EVP_MD_CTX_destroy(p->ctx);
OPENSSL_free(p);
}
static void sign_init(Options *opt, void **param, long proc_index) {
SignParam *p;
EVP_PKEY *pkey;
p = OPENSSL_malloc(sizeof(SignParam));
fail_if(!p, "OPENSSL_malloc() fail\n");
memset(p, 0, sizeof(SignParam));
p->in = BIO_new_file(opt->key, "r");
fail_clean_if(!p->in, sign_clean, (void *)p,
"BIO_new_file(%s) fail\n", opt->key);
pkey = PEM_read_bio_PrivateKey(p->in, NULL, NULL, NULL);
fail_clean_if(!pkey, sign_clean, (void *)p,
"PEM_read_bio_PrivateKey() fail\n");
p->ctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!p->ctx) {
EVP_PKEY_free(pkey);
fail_clean_if(1, sign_clean, (void *)p, "EVP_PKEY_CTX_new() fail\n");
}
fail_clean_if(!EVP_PKEY_sign_init(p->ctx), sign_clean, (void *)p,
"EVP_PKEY_sign_init() fail\n");
fail_clean_if(RAND_bytes(p->data, sizeof(p->data)) <= 0, sign_clean, (void *)p,
"RAND_bytes() fail\n");
p->data_len = sizeof(p->data);
p->sig_len = sizeof(p->sig);
p->index = proc_index;
*param = p;
}
static void sign_work(void *param) {
SignParam *p = (SignParam *)param;
fail_clean_if(!EVP_PKEY_sign(p->ctx, p->sig, &p->sig_len, p->data,
p->data_len), sign_clean, param,
"EVP_PKEY_sign() fail\n");
}
static void sign_clean(void *param) {
SignParam *p = (SignParam *)param;
if (p->ctx)
EVP_PKEY_CTX_free(p->ctx);
if (p->in)
BIO_free(p->in);
OPENSSL_free(p);
}
static void verify_init(Options *opt, void **param, long proc_index) {
VerifyParam *p;
EVP_PKEY *pkey;
X509 *x;
unsigned char data[48];
size_t data_len;
unsigned char sig[256];
size_t sig_len;
/* 先签名一次,获取数据 */
SignParam *sp;
sign_init(opt, (void **)&sp, (long)0);
sign_work(sp);
memcpy(data, sp->data, sp->data_len);
data_len = sp->data_len;
memcpy(sig, sp->sig, sp->sig_len);
sig_len = sp->sig_len;
sign_clean(sp);
p = OPENSSL_malloc(sizeof(VerifyParam));
fail_if(!p, "OPENSSL_malloc() fail\n");
memset(p, 0, sizeof(VerifyParam));
p->in = BIO_new_file(opt->cert, "r");
fail_clean_if(!p->in, verify_clean, (void *)p,
"BIO_new_file(%s) fail\n", opt->cert);
x = PEM_read_bio_X509(p->in, NULL, 0, NULL);
fail_clean_if(!x, verify_clean, (void *)p,
"PEM_read_bio_X509() fail\n");
pkey = X509_get_pubkey(x);
X509_free(x);
fail_clean_if(!pkey, verify_clean, (void *)p, "X509_get_pubkey() fail\n");
p->ctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!p->ctx) {
EVP_PKEY_free(pkey);
fail_clean_if(1, verify_clean, (void *)p, "EVP_PKEY_CTX_new() fail\n");
}
fail_clean_if(!EVP_PKEY_verify_init(p->ctx), verify_clean, (void *)p,
"EVP_PKEY_verify_init() fail\n");
memcpy(p->data, data, data_len);
p->data_len = data_len;
memcpy(p->sig, sig, sig_len);
p->sig_len = sig_len;
p->index = proc_index;
*param = p;
}
static void verify_work(void *param) {
VerifyParam *p = (VerifyParam *)param;
fail_clean_if(!EVP_PKEY_verify(p->ctx, p->sig, p->sig_len, p->data,
p->data_len), verify_clean, param,
"EVP_PKEY_verify() fail\n");
}
static void verify_clean(void *param) {
VerifyParam *p = (VerifyParam *)param;
if (p->ctx)
EVP_PKEY_CTX_free(p->ctx);
if (p->in)
BIO_free(p->in);
OPENSSL_free(p);
}
static void encrypt_init(Options *opt, void **param, long proc_index) {
EncParam *p;
EVP_PKEY *pkey;
X509 *x;
p = OPENSSL_malloc(sizeof(EncParam));
fail_if(!p, "OPENSSL_malloc() fail\n");
memset(p, 0, sizeof(EncParam));
p->in = BIO_new_file(opt->cert, "r");
fail_clean_if(!p->in, encrypt_clean, (void *)p,
"BIO_new_file(%s) fail\n", opt->cert);
x = PEM_read_bio_X509(p->in, NULL, 0, NULL);
fail_clean_if(!x, encrypt_clean, (void *)p,
"PEM_read_bio_X509() fail\n");
pkey = X509_get_pubkey(x);
X509_free(x);
fail_clean_if(!pkey, encrypt_clean, (void *)p, "X509_get_pubkey() fail\n");
p->ctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!p->ctx) {
EVP_PKEY_free(pkey);
fail_clean_if(1, encrypt_clean, (void *)p, "EVP_PKEY_CTX_new() fail\n");
}
fail_clean_if(!EVP_PKEY_encrypt_init(p->ctx), encrypt_clean, (void *)p,
"EVP_PKEY_encrypt_init() fail\n");
fail_clean_if(RAND_bytes(p->data, sizeof(p->data)) <= 0, encrypt_clean, (void *)p,
"RAND_bytes() fail\n");
p->data_len = sizeof(p->data);
p->enc_len = sizeof(p->enc);
p->index = proc_index;
*param = p;
}
static void encrypt_work(void *param) {
EncParam *p = (EncParam *)param;
fail_clean_if(!EVP_PKEY_encrypt(p->ctx, p->enc, &p->enc_len, p->data,
p->data_len), encrypt_clean, param,
"EVP_PKEY_encrypt() fail\n");
}
static void encrypt_clean(void *param) {
EncParam *p = (EncParam *)param;
if (p->ctx)
EVP_PKEY_CTX_free(p->ctx);
if (p->in)
BIO_free(p->in);
OPENSSL_free(p);
}
static void decrypt_init(Options *opt, void **param, long proc_index) {
DecParam *p;
EVP_PKEY *pkey;
unsigned char data[48];
size_t data_len;
unsigned char enc[256];
size_t enc_len;
/* 先加密一次,获取数据 */
EncParam *ep;
encrypt_init(opt, (void **)&ep, (long)0);
encrypt_work(ep);
memcpy(data, ep->data, ep->data_len);
data_len = ep->data_len;
memcpy(enc, ep->enc, ep->enc_len);
enc_len = ep->enc_len;
encrypt_clean(ep);
p = OPENSSL_malloc(sizeof(DecParam));
fail_if(!p, "OPENSSL_malloc() fail\n");
memset(p, 0, sizeof(DecParam));
p->in = BIO_new_file(opt->key, "r");
fail_clean_if(!p->in, decrypt_clean, (void *)p,
"BIO_new_file(%s) fail\n", opt->key);
pkey = PEM_read_bio_PrivateKey(p->in, NULL, NULL, PEM_AUTO_KEYPASS);
fail_clean_if(!pkey, decrypt_clean, (void *)p,
"PEM_read_bio_PrivateKey() fail\n");
p->ctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!p->ctx) {
EVP_PKEY_free(pkey);
fail_clean_if(1, decrypt_clean, (void *)p, "EVP_PKEY_CTX_new() fail\n");
}
fail_clean_if(!EVP_PKEY_decrypt_init(p->ctx), decrypt_clean, (void *)p,
"EVP_PKEY_decrypt_init() fail\n");
memcpy(p->data, data, data_len);
p->data_len = data_len;
memcpy(p->enc, enc, enc_len);
p->enc_len = enc_len;
p->index = proc_index;
*param = p;
}
static void decrypt_work(void *param) {
DecParam *p = (DecParam *)param;
fail_clean_if(!EVP_PKEY_decrypt(p->ctx, p->data, &p->data_len, p->enc,
p->enc_len), decrypt_clean, param,
"EVP_PKEY_decrypt() fail\n");
}
static void decrypt_clean(void *param) {
DecParam *p = (DecParam *)param;
if (p->ctx)
EVP_PKEY_CTX_free(p->ctx);
if (p->in)
BIO_free(p->in);
OPENSSL_free(p);
}
multi-process.c
#include <sys/wait.h> // wait
#include <sys/types.h> // getpid, wait
#include <sys/ipc.h> // shmget, shmat, shmctl, shmdt
#include <sys/shm.h> // shmget, shmat, shmctl, shmdt
#include <signal.h> // sigaction, SIGLARM
#include <limits.h> // LONG_MIN, LONG_MAX, ULLONG_MAX
#include <errno.h> // errno
#include <unistd.h> // getpid
#include <string.h> // memset
#include "common.h"
#include "opt.h"
#include "work.h"
int isStop = 0; // 用于标记测试终止
Options opt; // 命令行选项
int shmid; // 共享内存id
Result *shm = NULL; // 共享内存地址,用于存放测试结果
Result res_total;
Result res_last;
void handle_signal_child(int sigNum)
{
if (sigNum == SIGALRM) {
isStop = 1;
}
}
void handle_signal_parent(int sigNum)
{
if (sigNum == SIGALRM) {
/* DO REAL-TIME STATISTICS */
memset(&res_total, 0, sizeof(Result));
long i = 0;
for (; i < opt.procs; ++i) {
res_total.count += shm[i].count;
}
mylog("total count %12llu, average %12.0lf/s\n",
res_total.count, (res_total.count - res_last.count)
/ (double)opt.interval);
memcpy(&res_last, &res_total, sizeof(Result));
/* DO REAL-TIME STATISTICS */
alarm(opt.interval);
}
}
/* 执行测试主循环函数 */
void doTest(void *param) {
unsigned long long i = 0;
HashParam *pa = (HashParam *)param;
for (; i < ULLONG_MAX && !isStop; ++i) {
/* DO YOUR WORK */
test_work(param);
++shm[pa->index].count;
/* DO YOUR WORK */
}
}
int main(int argc, char *argv[]) {
int rv = 0;
long i = 0;
int proc_index = 0;
int isParent = 1;
int wstatus = 0;
pid_t pid = 0;
struct sigaction act_child;
struct sigaction act_parent;
rv = process_options(argc, argv, &opt);
if (rv) {
return -1;
}
mylog("\n-----------------------------Start Testing-----------------------"
"-------\n\n");
/* COMMON INIT */
shmid = shmget(IPC_PRIVATE, sizeof(sizeof(Result) * opt.procs), 0666);
fail_if(-1 == shmid, "shmget() failed\n");
mylog("shmid = %d\n", shmid);
shm = (Result*)shmat(shmid, 0, 0);
fail_if((void *) -1 == shm, "shmat() failed\n");
/* 这里直接进行IPC_RMID操作,进程退出之后会自动detach了, 从而释放共享内存 */
shmctl(shmid, IPC_RMID, 0);
memset(shm, 0, sizeof(sizeof(Result) * opt.procs));
global_init(&opt);
/* COMMON INIT */
while(isParent && i < opt.procs) {
pid = fork();
fail_if(-1 == pid, "fork failed %d\n", pid); /* error */
if(pid == 0) { /* child */
isParent = 0;
proc_index = i; // 记录进程索引
}
else { /* parent */
}
++i;
}
if(isParent) {
/* PARENT INIT */
memset(&act_parent, 0, sizeof(act_parent));
act_parent.sa_handler = handle_signal_parent;
/* 使wait被中断时可以自动恢复 */
act_parent.sa_flags = SA_RESTART;
rv = sigaction(SIGALRM, &act_parent, NULL); // 用于定时统计结果
fail_if(rv, "sigaction() failed\n");
memset(&res_last, 0, sizeof(Result));
alarm(opt.interval);
/* PARENT INIT */
/* DO FINAL STATISTICS */
Result final;
memset(&final, 0, sizeof(Result));
for(i =0 ; i < opt.procs; ++i) {
pid = wait(&wstatus); // 等待子进程结束
alarm(0); // 终止定时器
fail_if(-1 == pid, "wait() failed, errno=%d\n", errno);
mylog("process [pid = %6d] exit\n", pid);
mylog("process [pid = %6u] count %12llu in %lus,"
" average %12.0lf/s\n", pid, shm[i].count, opt.duration,
shm[i].count / (double)opt.duration);
final.count += shm[i].count;
}
mylog("total count %12llu in %lus, average %12.0lf/s\n",
final.count, opt.duration, final.count / (double)opt.duration);
/* DO FINAL STATISTICS */
/* PARENT CLEANUP */
global_clean();
/* PARENT CLEANUP */
}
else {
/* CHILD INIT */
void *param;
test_init(&opt, ¶m, proc_index);
/* CHILD INIT */
act_child.sa_handler = handle_signal_child;
sigemptyset(&act_child.sa_mask);
act_child.sa_flags = SA_RESETHAND;
/* 用于测试时间到时,通知子进程结束测试 */
rv = sigaction(SIGALRM, &act_child, NULL);
fail_if(rv, "sigaction() failed\n");
alarm(opt.duration); // 设置测试时长
doTest(param);
/* CHILD CLEANUP */
test_clean(param);
global_clean();
/* CHILD CLEANUP */
return 0; /* child finished work */
}
return 0;
}
总结
至此,一个多进程的测试程序就算完成了。读者可以根据自身测试需要,按如下步骤修改以进行自定义的测试。
- 在
opt.c
和opt.h
中,添加需要的命令行选项 - 在
work.c
和work.h
中,global_init()
和global_clean()
内进行全局的初始化及全局的清理工作 - 在
work.c
和work.h
中,添加自定义的测试函数,以及相应的测试参数和测试结果 - 主控
multi-process.c
通过test_init()
、test_work()
、test_clean()
调用测试相关的初始化、执行及清理工作 - 主控
multi-process.c
中,修改测试结果的更新与统计操作。
主控multi-process.c
中:
COMMON INIT
、PARENT INIT
和CHILD INIT
代码块处分别进行公共的初始化工作和父子进程特定的初始化工作。PARENT CLEANUP
和CHILD CLEANUP
代码块处则分别进行对应的清理工作。DO YOUR WORK
处执行具体的测试,DO REAL-TIME STATISTICS
和DO FINAL STATISTICS
处进行测试结果的统计。
当然本文还有一些不足之处,比如当前是用的OpenSSL1.0.2接口,没有兼容不同版本的OpenSSL。还有代码风格的问题,在函数命名和注释方式上不统一。但是考虑到这个是测试程序,就不计较这么多了(。)