Linux学习之悟空派上实现OLED的无线网IP及CPU温度显示【守护进程】
起因
最近各种网购平台似乎都在推送99元的悟空派全志H3的开发板,出于好奇就买了一块来试试水,由于这块板子基本上和orangepi-Zero的硬件结构一模一样,所以设备树、boot这些就用orangepi现成的部件了。
因为本人比较喜欢使用SSH操作,但是在不同环境下使用连接的WiFi不一样,所以对应的IP地址也就不一样,所以我给悟空派弄了一个0.98寸的OLED,用来显示CPU温度和当前IP地址,并5秒会刷新一次数据内容,为我连接SSH提供了一定的帮助,但是由于该任务是开机启动的前台任务,就导致我的串口Shell始终处于显示该线程打印数据内容的状态下,导致我无法在新环境连接新WiFi,所以考虑如何解决该问题。
处理方案
由于该任务我希望从设备启动时运行到设备关机时关闭,不希望其与终端产生关联,故在linux环境中找到较为合适的处理方案便为将守护进程。
守护进程(Daemon)
代蒙(希腊文:δαίμων、拉丁文:Dæmon、英文:Daemon)是希腊神话中的一种介于神与人之间的精灵或妖魔。它们与神祇的区别在于精灵并不具有人的外貌,而是一种善恶并存的超自然存在。在罗马神话中,代蒙称为格尼烏斯(Genius)。
代蒙无处不在,伴随着人的一生。 根据古希腊唯心主义哲学派的解释,人一生下来一直到死亡都有代蒙伴随并支配他的一切行动。
从这个英文单词的意义可以得知,daemon在linux系统中就是伴随linux运行一生并支配其行动的东西,这个东西善恶并存,是否就可以理解成daemon的好坏取决于编写daemon的我们,且无论好坏都将伴随linux的整个运行周期。
下面是标准理解,此处借鉴了大佬 JMW1407文章中 【Linux】守护进程( Daemon)的定义,作用,创建流程_daemon的作用-CSDN博客
1、定义
守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或循环等待处理某些事件的发生;它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机才随之一起停止运行;
守护进程一般都以root用户权限运行,因为要使用某些特殊的端口(1-1024)或者资源;
守护进程的父进程一般都是init进程,因为它真正的父进程在fork出守护进程后就直接退出了,所以守护进程都是孤儿进程,由init接管;
守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
守护进程的名称通常以d结尾,比如sshd、xinetd、crond等
2、作用
- 守护进程是一个生存周期较长的进程,通常独立于控制终端并且周期性的执行某种任务或者等待处理某些待发生的事件
- 大多数服务都是通过守护进程实现的
- 关闭终端,相应的进程都会被关闭,而守护进程却能够突破这种限制
功能实现
基本源码
首先大家可以看一下我是如何在悟空派使用OLED显示IP地址和温度的,源代码如下
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <syslog.h>
#define MAXFILE 65535
#include <netinet/in.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include "oled.h"
#include "font.h"
#define TEMP_PATH "/sys/class/thermal/thermal_zone0/temp"
#define MAX_SIZE 32
#define ETH_NAME "wlan0"
double temp_get(void){
int fd;
double temp = 0;
char buf[MAX_SIZE];
// 开/sys/class/thermal/thermal_zoneo/temp
fd = open(TEMP_PATH,O_RDONLY);
if (fd < 0) {
fprintf(stderr,"failed to open thermal_zoneo/tempin");
return -1;
}
//读取内容
if(read(fd,buf,MAX_SIZE) < 0) {
fprintf(stderr,"fatled to read tempin");
return -1;
}
// 转换为浮点数打印
temp = atoi(buf);
temp /= 1000;
// printf("temp: %.2f\n", temp);
//关闭文件
close(fd);
return temp;
}
//获取当前IP地址
char* get_local_ip()
{
int sock;
struct sockaddr_in sin;
struct ifreq ifr;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
perror("socket");
return NULL;
}
strncpy(ifr.ifr_name, ETH_NAME, IFNAMSIZ);
ifr.ifr_name[IFNAMSIZ - 1] = 0;
if (ioctl(sock, SIOCGIFADDR, &ifr) < 0) {
perror("ioctl");
return NULL;
}
memcpy(&sin, &ifr.ifr_addr, sizeof(sin));
return inet_ntoa(sin.sin_addr);
}
//OLED显示的基本框架
int oled_demo(struct display_info *disp) {
int i = 0;
char buf[100];
double temp = 0;
//从系统获取并在OLED上显示温度
temp = temp_get();
disp->font = font3;
sprintf(buf, "CPU-Temp: %.2f C", temp);
oled_putstrto(disp, 0, 0, buf);
//从系统中获取并在OLED显示当前IP地址
char* local_ip = get_local_ip();
sprintf(buf, "WIP:%s", local_ip);
oled_putstrto(disp, 0, 10, buf);
//显示博主本人ID
disp->font = font1;
oled_putstrto(disp, 0, 20, "----ASWaterbenben----");
//打印分隔符
disp->font = font2;
for(i=0;i<128;i++)
oled_putpixel(disp, i, 30, 1);
oled_putstrto(disp, 0, 32, " ");
oled_putstrto(disp, 0, 37, "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ");
//打印结束标记
disp->font = font1;
oled_putstrto(disp, 0, 54, "*********************");
oled_send_buffer(disp);
return 0;
}
//故障输出暂不启用
void show_error(int err, int add) {
//const gchar* errmsg;
//errmsg = g_strerror(errno);
// printf("\nERROR: %i, %i\n\n", err, add);
//printf("\nERROR\n");
}
//用户输出暂不启用
void show_usage(char *progname) {
// printf("\nUsage:\n%s <I2C bus device node >\n", progname);
}
int main(int argc, char **argv) {
int i;
int e;
char filename[32];
struct display_info disp;
if (argc < 2) {
show_usage(argv[0]);
return -1;
}
//清空显示句柄
memset(&disp, 0, sizeof(disp));
sprintf(filename, "%s", argv[1]);
disp.address = OLED_I2C_ADDR;
disp.font = font2;
//打开OLED的设备驱动
e = oled_open(&disp, filename);
char *buf = "OLED fresh\r\n";
int len = strlen(buf);
if (e < 0) {
show_error(1, e);
}
else {
//OLED设备初始化
e = oled_init(&disp);
if (e < 0) {
show_error(2, e);
}
else {
//循环每隔5秒显示一次DEMO
while(1)
{
oled_demo(&disp);
sleep(5);
}
}
}
return 0;
}
显示效果
效果如下图所示
但是在终端启动该功能后,将终端关闭则该进程也就同时关闭,并未达到预期效果,故考虑使用守护进程的处理方法进行修改;
守护进程实现逻辑和源码
脱离终端控制
由于在终端中启动的进程寿命与终端一致。终端被关闭则进程也被关闭,故首先需要脱离终端控制;
使用umask(0);为当前进程获取最大访问权限
使用fork();函数创建子进程,使用exit(0);退出父进程
完成上述操作后,终端认为父进程已退出,此时可继续进行终端操作。
独立进程
父进程退出后虽然终端可以继续操作,但是新建的子进程依旧归属于父进程所在的进程组、会话期、控制终端。故需要是因setsid();函数将子进程从父进程组中独立出来。
由于此时没有终端作为进程状态输出界面,故需要以其他形式将当前进程反馈的信息打印并保存,便于后续检查线程的运行情况,故使用openlog(“daemon”, LOG_PID, LOG_DAEMON);打开日志服务;
进程中调用 chdir() 函数,让根目录 ”/”
成为当前进程的工作目录 ,防止线程源文件所在位置是可卸载的存储介质,若介质被移除导致进程终端的问题。
进程垃圾处理
清除父进程连带可能产生的已启动文件,由于父进程已经关闭,但是当前进程会继承父进程已经打开的文件,这些文件当前进程并未使用,则将所有继承下来已打开的进程完全关闭。
加入功能代码
/*
* Copyright (c) 2015, Vladimir Komendantskiy
* MIT License
*
* SSD1306 demo of block and font drawing.
*/
//
// fixed for OrangePiZero by HypHop
//
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <syslog.h>
#define MAXFILE 65535
#include <netinet/in.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include "oled.h"
#include "font.h"
#define TEMP_PATH "/sys/class/thermal/thermal_zone0/temp"
#define MAX_SIZE 32
#define ETH_NAME "wlan0"
double temp_get(void){
int fd;
double temp = 0;
char buf[MAX_SIZE];
// 开/sys/class/thermal/thermal_zoneo/temp
fd = open(TEMP_PATH,O_RDONLY);
if (fd < 0) {
fprintf(stderr,"failed to open thermal_zoneo/tempin");
return -1;
}
//读取内容
if(read(fd,buf,MAX_SIZE) < 0) {
fprintf(stderr,"fatled to read tempin");
return -1;
}
// 转换为浮点数打印
temp = atoi(buf);
temp /= 1000;
// printf("temp: %.2f\n", temp);
//关闭文件
close(fd);
return temp;
}
char* get_local_ip()
{
int sock;
struct sockaddr_in sin;
struct ifreq ifr;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
perror("socket");
return NULL;
}
strncpy(ifr.ifr_name, ETH_NAME, IFNAMSIZ);
ifr.ifr_name[IFNAMSIZ - 1] = 0;
if (ioctl(sock, SIOCGIFADDR, &ifr) < 0) {
perror("ioctl");
return NULL;
}
memcpy(&sin, &ifr.ifr_addr, sizeof(sin));
return inet_ntoa(sin.sin_addr);
}
int oled_demo(struct display_info *disp) {
int i = 0;
char buf[100];
double temp = 0;
temp = temp_get();
disp->font = font3;
sprintf(buf, "CPU-Temp: %.2f C", temp);
oled_putstrto(disp, 0, 0, buf);
//putstrto(disp, 0, 0, "Spnd spd 2468 rpm");
// oled_putstrto(disp, 0, 9+1, "Spnd cur 0.46 A");
char* local_ip = get_local_ip();
sprintf(buf, "WIP:%s", local_ip);
oled_putstrto(disp, 0, 10, buf);
disp->font = font1;
// oled_putstrto(disp, 0, 18+2, "Spnd tmp 53 C");
oled_putstrto(disp, 0, 20, "----ASWaterbenben----");
disp->font = font2;
// oled_putstrto(disp, 0, 27+3, "DrvX tmp 64 C");
for(i=0;i<128;i++)
oled_putpixel(disp, i, 30, 1);
oled_putstrto(disp, 0, 32, " ");
oled_putstrto(disp, 0, 37, "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ");
disp->font = font1;
// oled_putstrto(disp, 0, 54, "Total cur 2.36 A");
oled_putstrto(disp, 0, 54, "*********************");
oled_send_buffer(disp);
disp->font = font3;
// for (i=0; i<100; i++) {
// sprintf(buf, "Spnd spd %d rpm", i);
// oled_putstrto(disp, 0, 0, buf);
// oled_putstrto(disp, 135-i, 36+4, "===");
// oled_putstrto(disp, 100, 0+i/2, ".");
// oled_send_buffer(disp);
// }
//oled_putpixel(disp, 60, 45);
//oled_putstr(disp, 1, "hello");
return 0;
}
void show_error(int err, int add) {
//const gchar* errmsg;
//errmsg = g_strerror(errno);
// printf("\nERROR: %i, %i\n\n", err, add);
//printf("\nERROR\n");
}
void show_usage(char *progname) {
// printf("\nUsage:\n%s <I2C bus device node >\n", progname);
}
int main(int argc, char **argv) {
printf("pid = %d\n", getpid());
int i;
int fd;
pid_t pid;
// 第一步
umask(0);
// 第二步
pid = fork();
if(pid < 0) {
perror("fork error!");
exit(1);
} else if(pid > 0) {
exit(0);
}
// 打开系统日志服务
openlog("daemon", LOG_PID, LOG_DAEMON);
// 第三步
setsid();
// 第四步
chdir("/");
// 第五步
for (i = 0; i < MAXFILE; ++i) {
close(i);
}
signal(SIGCHLD,SIG_IGN);
int e;
char filename[32];
struct display_info disp;
if (argc < 2) {
show_usage(argv[0]);
return -1;
}
memset(&disp, 0, sizeof(disp));
sprintf(filename, "%s", argv[1]);
disp.address = OLED_I2C_ADDR;
disp.font = font2;
e = oled_open(&disp, filename);
char *buf = "OLED fresh\r\n";
int len = strlen(buf);
if (e < 0) {
show_error(1, e);
}
else {
e = oled_init(&disp);
if (e < 0) {
show_error(2, e);
}
else {
while(1)
{
oled_demo(&disp);
fd = open("/tmp/deamon.log", O_CREAT|O_WRONLY|O_
if (fd < 0) {
syslog(LOG_ERR, "open");
exit(1);
}
write(fd, buf, len+1);
close(fd);
sleep(5);
}
}
}
closelog();
return 0;
}
编译与执行
使用gcc命令将修改好的c文件编译为out文件,并将out文件加入到开机自启任务中
即在 /etc/rc.local中的exit(0);前添加/home/orangepi/Cprogram/temp/oled.sh(oled.sh为已经授权的sh运行脚本,运行脚本核心是将编译完成的执行文件带入硬件固有的I2C硬件接口),oled.sh内容如下:
#!/bin/bash
cd /home/orangepi/Cprogram/temp
./oled_demo /dev/i2c-0
/home/orangepi/Cprogram/temp为执行文件所在路径
oled_demo就是编译完成的执行文件,/dev/i2c-0为OLED屏幕当前使用的硬件I2C接口。
效果
效果就不太好展示了,大概口头说一下,就是我的悟空派在启动后OLED就被点亮,并在屏幕上显示当前CPU温度、当前无线网IP地址,同时串口终端也成正常终端的状态,每次屏幕内容刷新都会在/tmp/deamon.log日志文件中打印一条消息OLED fresh。