在与外部C++程序对接的时候,日志输出是其中关键一环,如果我们想要在端侧去采集一些日志数据,一般都会有自己的日志接口。但是一般非针对Android平台的C++程序输出日志时,都会采取std::out的方式进行日志的输出,比如
c
std::cout<< "hello pika"<<std::endl;
但是这些日志无法保存,没能对接到自己的日志系统中,如果日志比较重要,那么把所有的std::out都改成自定义接口的方式实现,不仅成本大 而且后续接口变化的时候还会增加二次修改,下面我们介绍一个重定向的小技巧,帮助大家在Native开发中更好的处理日志需求
实现对标准输出的重定向
std::cout是C++ 标准库中用于标准输出的对象,那么最终的内容保存在哪里呢?肯定有一个文件载体对不对。这些日志都会被存入在文件系统的"文件中",而保存标准输出的文件有一个FD标识,它就是STDOUT_FILENO
。 当然,预定义的FD标识比如标准输入,标准错误输出,都是有相应的文件标识符
arduino
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
我们要做的,其实就是把STDOUT_FILENO所属的文件内容复制一份到我们自定义的接口即可。这里我们会用到两个系统调用,dup2跟pipe
- dup2函数声明为
int dup2(int oldfd, int newfd)
。 oldfd
:是源文件描述符,也就是要被复制的文件描述符。这个文件描述符必须是一个有效的、已经打开的文件描述符。newfd
:是目标文件描述符,dup2
函数会将oldfd
复制到newfd
。如果newfd
已经打开,dup2
会先关闭newfd
,然后再进行复制操作,此后,newfd跟oldfd共同标识着同一个文件。
大家可以从man7.org/linux/man-p... 获取更多关于dup2系统调用的信息。
当然,进行了重定向还不够,我们最好能够监听这个文件的输入,以便我们可以把想要的内容可以直接对接到其他日志系统中,如果没有这一步,那么我们只是把东西再往另一个文件写了一遍而已。这里的关键就是pipe
pipe
函数的声明为int pipe(int pipefd[2])
。其中,pipefd
是一个长度为2
的整数数组,用于存储管道的两个端点的文件描述符。pipefd[0]
:用于从管道读取数据,被称为管道的读端。pipefd[1]
:用于向管道写入数据,被称为管道的写端。
我们可以通过dup2把标准输出的内容复制到管道的写入数据端,这样我们就能够从管道的读取端读取数据了。为了避免std::out写入缓存的情况,我们还需要把通过setvbuf(stdout, 0, _IONBF, 0)把缓冲区设置为0,这样数据就能做到实时同步避免产生数据丢失。
当然,读取属于io操作,因此一般情况下我们会另外新建立一个线程,在子线程中读取数据从而不干扰到其他线程。
scss
int pipe_stdout[2];
int pipe_stderr[2];
pthread_t thread_stdout;
int redirecting_stdout() {
//set stdout as unbuffered.
setvbuf(stdout, 0, _IONBF, 0);
pipe(pipe_stdout);
dup2(pipe_stdout[1], STDOUT_FILENO);
//https://man7.org/linux/man-pages/man2/dup.2.html
if(pthread_create(&thread_stdout, nullptr, thread_stdout_func, nullptr) != 0)
return -1;
// 注意分离线程,或者创建时指定线程分离即可pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED)
pthread_detach(thread_stdout);
return 0;
}
这里我们假设对接的日志输出接口是Android中的log,我们只需要源源不断从管道读取端读取数据接口,这里的数据就可以被我们对接到任意的日志系统中。
arduino
void *thread_stdout_func(void*) {
// 设置合适的buffer大小
char buffer[1024];
ssize_t bytesRead;
while (true) {
// 不断读取管道的内容即可
bytesRead = read(pipe_stdout[0], buffer, sizeof(buffer));
if (bytesRead > 0) {
// 对读取到的数据进行处理,比如输出
buffer[bytesRead] = 0;
// 假设我们的日志接口对接到android log日志系统中 ...
__android_log_write(ANDROID_LOG_INFO, "hello", buffer);
continue;
} else if (bytesRead == 0) {
// 到达文件末尾,正常结束循环
break;
} else if (bytesRead == -1) {
if (errno == EINTR) {
// 如果是因为信号中断导致读取错误,重新尝试读取
continue;
} else {
// 其他错误情况,打印错误信息并退出循环
__android_log_write(ANDROID_LOG_INFO, "hello", "error");
break;
}
}
}
return nullptr;
}
为了方便管理,在需要进行标准重定向时,我们才调用这个重定向方法即可,这里可以提供一个jni方法,由外部控制是否要进行输出重定向操作
arduino
extern "C"
JNIEXPORT void JNICALL
Java_com_pika_mooner_redirecting_stdout(JNIEnv *env, jobject thiz) {
redirecting_stdout();
}
小结
上述提到的重定向小技巧可以也可以用在其他标准输入或者错误输出甚至自定义文件中,在日常开发中我们很有可能需要对接非面向于Android的C++库,运用这个小技巧我们可以把这些库的日志重定向到任意日志系统中,方便我们开发与回收日志。