1.需求背景
因为项目需要,产品售卖到国外各个地区,需要能适配各个国家的不同时区,一些国家可能会有多个不同时区,并且还存在冬夏令时问题,都需要做到一次性的兼容。而目前板子上可用的flash空间也已经不足200KB,需要同时考虑对flash空间节省。
网上的资料并不齐全,这里完成后特地进行总结。
这里需要做到,例如收到Australia/Canberra后,需要得出对应时区的UTC偏移,然后更改系统时间,并且是需要适应冬夏令时的情况。
按照网上资料进行zoneinfo移植并设置时区后,发现时间并不对,后来查找资料发现uclibc和glibc对时区的使用有差异:
1.对于uclibc,重要文件是/etc/TZ,实际连接到/tmp/TZ,修改时区后,会根据配置文件system中的timezone的option修改/tmp/TZ
2.对于glibc,重要文件是/etc/localtime,实际连接到/tmp/localtime,而/tmp/localtime也是连接文件,根据配置文件system中的zonename的option修改该连接
而我是uclibc,所以方法会更复杂些,glibc的话会更简单,因为在尝试的过程中都试了,所以一并总结下。
2.方案实现
最终逻辑为:交叉编译timezoneinfo数据库后进行tar.bz2的压缩,节约出flash空间,再放到目标板中,当拿到服务端传来的Asia/Shanghai后,解压数据库到内存中,完成时区信息匹配后,再将内存中的文件删除,达成目的。
数据库timezoneinfo其中记载了全球各个地区的时区信息,以及冬夏令时信息等。
其中有tzdata2024a.tar.gz和tzcode2024a.tar.gz,tzdata是时区信息的一个数据库,而tzcode是时区的命令以及用于生成数据库的工具,需要用其中的工具,在target上生成出时区数据库。下载地址https://www.iana.org/time-zones
2.1 下载源码、数据库
mkdir /tmp/zoneinfo
tar -vxf tzcode2024a.tar.gz -C /tmp/zoneinfo
tar -vxf tzdata2024a.tar.gz -C /tmp/zoneinfo
解压后大致内容如下:
2.2 编译源码
因为要移植到目标板上,所以先在虚拟机上make 并make install看下执行了哪些操作,再用交叉编译工具链编译,并安装到目标板上。
2.2.1 虚拟机上编译
make
make install
install时大概执行的内容如下:
make BACKWARD='backward' DESTDIR='' LEAPSECONDS='' PACKRATDATA='' PACKRATLIST='' TZDEFAULT='/etc/localtime' TZDIR='/usr/share/zoneinfo' ZIC='./zic ' LEAPSECONDS= install_data
make[1]: Entering directory '/tmp/zoneinfo'
./zic -d '/usr/share/zoneinfo' tzdata.zi
make[1]: Leaving directory '/tmp/zoneinfo'
rm -fr '/usr/share/zoneinfo-posix'
ln -s 'zoneinfo' '/usr/share/zoneinfo-posix' || \
make BACKWARD='backward' DESTDIR='' LEAPSECONDS='' PACKRATDATA='' PACKRATLIST='' TZDEFAULT='/etc/localtime' TZDIR='/usr/share/zoneinfo' ZIC='./zic ' TZDIR='/usr/share/zoneinfo-posix' posix_only
make BACKWARD='backward' DESTDIR='' LEAPSECONDS='' PACKRATDATA='' PACKRATLIST='' TZDEFAULT='/etc/localtime' TZDIR='/usr/share/zoneinfo' ZIC='./zic ' TZDIR='/usr/share/zoneinfo-leaps' right_only
make[1]: Entering directory '/tmp/zoneinfo'
make BACKWARD='backward' DESTDIR='' LEAPSECONDS='' PACKRATDATA='' PACKRATLIST='' TZDEFAULT='/etc/localtime' TZDIR='/usr/share/zoneinfo-leaps' ZIC='./zic ' LEAPSECONDS='-L leapseconds' \
install_data
make[2]: Entering directory '/tmp/zoneinfo'
./zic -d '/usr/share/zoneinfo-leaps' -L leapseconds tzdata.zi
make[2]: Leaving directory '/tmp/zoneinfo'
make[1]: Leaving directory '/tmp/zoneinfo'
mkdir -p '/usr/bin' \
'/usr/bin' '/usr/sbin' \
'/usr/lib' \
'/usr/share/man/man3' '/usr/share/man/man5' \
'/usr/share/man/man8'
./zic -d '/usr/share/zoneinfo' -l Factory \
`case '-' in ?*) echo '-p';; esac \
` - \
-t '/etc/localtime'
cp -f iso3166.tab leapseconds tzdata.zi zone.tab zone1970.tab zonenow.tab '/usr/share/zoneinfo/.'
cp tzselect '/usr/bin/.'
cp zdump '/usr/bin/.'
cp zic '/usr/sbin/.'
cp libtz.a '/usr/lib/.'
: '/usr/lib/libtz.a'
cp -f newctime.3 newtzset.3 '/usr/share/man/man3/.'
cp -f tzfile.5 '/usr/share/man/man5/.'
cp -f tzselect.8 zdump.8 zic.8 '/usr/share/man/man8/.'
那么在移植到嵌入式板上的时候,也是直接模仿这个过程
直接虚拟机上测试下效果。
export TZDIR="/usr/share/zoneinfo"
export TZ="Australia/Canberra"
date -R
能看到时间显示为+11时区,修改成功
2.2.2 移植到开发板上
在目标机上创建两个文件夹,zoneinfo用来放源码文件,zoneinfo_data用来存放生成的时区数据库文件
cd /tmp
mkdir zoneinfo
mkdir zoneinfo_data
虚拟机上先make clean一下,然后重新开始编译
sudo make CC=/home/xzx/share/project_ipc/hm1002_in/code/openwrt2/staging_dir/toolchain-mipsel_24kec+dsp_gcc-11.2.0_uClibc-0.9.33.2/bin/mipsel-openwrt-linux-uclibc-gcc CFLAGS="-DHAVE_GETTEXT=0 -DHAVE_GETRANDOM=0"
-DHAVE_GETTEXT=0 -DHAVE_GETRANDOM=0 是为了解决编译报错,undefined reference to textdomain'以及undefined reference to
dcgettext’问题。
编译通过后,将编译后的整个文件夹拷贝到目标板上的 /tmp/zoneinfo 上。
模仿虚拟机上的make install操作,进行时区数据库生成
./zic -d '/tmp/zoneinfo_data/zoneinfo' tzdata.zi
rm -fr '/tmp/zoneinfo_data/zoneinfo-posix'
ln -s '/tmp/zoneinfo_data/zoneinfo' '/tmp/zoneinfo_data/zoneinfo-posix'
./zic -d '/tmp/zoneinfo_data/zoneinfo-leaps' -L leapseconds tzdata.zi
mkdir -p '/usr/bin' \
'/usr/bin' '/usr/sbin' \
'/usr/lib' \
./zic -d '/tmp/zoneinfo_data/zoneinfo' -l Factory \
`case '-' in ?*) echo '-p';; esac \
` - \
-t '/etc/localtime'
cp -f iso3166.tab leapseconds tzdata.zi zone.tab zone1970.tab zonenow.tab '/tmp/zoneinfo_data/zoneinfo/.'
# 这里先确认下自己的目标板环境中有没有/etc/localtime文件。
# 如果是uclibc+openwet的环境,没有/etc/localtime文件,下面的指令全部不需要执行了!!执行了也没有作用,反而会影响最终结果!
# 这里也是我自己踩了很久坑的地方!
rm /usr/bin/tzselect /usr/bin/zdump /usr/sbin/zic /usr/lib/libtz.a
ln -s /tmp/zoneinfo/tzselect '/usr/bin/.'
ln -s /tmp/zoneinfo/zdump '/usr/bin/.'
ln -s /tmp/zoneinfo/zic '/usr/sbin/.'
ln -s /tmp/zoneinfo/libtz.a '/usr/lib/.'
: '/usr/lib/libtz.a'
export TZDIR="/tmp/zoneinfo_data/zoneinfo"
export TZ="Asia/Shanghai"
如果是glibc环境,执行完上面的之后,输入date -R便能成功看到时间发生了变化,这里之后的操作便不需要继续执行了,成功完成了数据库的移植与使用!
而如果是uclibc+openwrt环境,不需要执行上面下部分的命令,通过vi /tmp/zoneinfo_data/zoneinfo/Australia/Canberra 能看到最后一行便是对应的POSIX时区信息,将他们设置到openwrt环境中。
设置为openwrt的系统时间:
uci set system.@system[0].timezone='AEST-10AEDT,M10.1.0,M4.1.0/3'
uci commit system
/etc/init.d/system restart
root@OpenWrt:/tmp/zoneinfo# date -R
Wed, 06 Mar 2024 19:25:57 +1100
可以看到系统时间发生了变化,并且时间正确
2.2.3 TZ的格式
TZ = local_timezone,date/time,date/time
local_timezone是时区名称,其后两个date/time分别表示DST变更时间点(即何时开始,何时结束),date格式为Mm.n.d(注:“M”是字符),其中m范围为1-12月份,如M3表示3月份;n范围为1-5,1表示一个月中第一周,5表示最后一周;d范围为0~6,0表示星期日,6表示星期六。time为hh:mm:ss的格式。
则AEST-10AEDT,M10.1.0,M4.1.0/3代表 AEST-10AEDT时区,从第十个月开始的第一周的星期天开始变更为夏令时,从第四个月的第一周的星期天3点钟结束
3.压缩数据库并使用
因为板子上的flash空间不足,所以决定以压缩包形式放入,待需要时再解压出来。如果没有这个需求的话,可以不用看。
1)数据库文件传回虚拟机进行压缩
scp * xzx@192.168.80.228:/tmp/zoneinfo_data/
这里我是在虚拟机上进行.tar.bz2压缩
tar -vcjf zoneinfo_data.tar.bz2 /tmp/zoneinfo_data
压缩前的zoneinfo_data大约2.8MB,压缩后的zoneinfo_data.tar.bz2在95KB左右
2)用程序解压压缩包,并设置时区
uclibc+openwrt可以参考一下代码
#include <stdio.h>
#include <bzlib.h>
#include <string>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdarg.h>
#include <sys/time.h>
#include <signal.h>
#include<stdexcept>
bool linuxPopenExecCmd(std::string &strOutData, const char * pFormat, ...)
{
char acBuff[128] ={0};
va_list ap;
va_start(ap, pFormat);
vsprintf(acBuff, pFormat, ap);
va_end(ap);
try {
FILE *pFile = popen(acBuff, "r");
if (!pFile) {
throw std::runtime_error("linuxPopenExecCmd popen() failed!");
}
char acValue[512] = {0};
while (!feof(pFile)) {
if (fgets(acValue, sizeof(acValue), pFile) != nullptr) {
strOutData += acValue;
}
}
pclose(pFile);
}
catch (const std::exception& e)
{
printf("popen :%s failed: %s", acBuff, e.what());
return false;
}
return true;
}
int deCompress(const char *srcFile, const char *dstFile)
{
int iRet = 0;
FILE *fileInput = NULL;
FILE *fileOutput = NULL;
BZFILE *bzInput = NULL;
int iBytesRead = 0;
int iResult = 0;
char acBuffer[1024] = {0};
fileInput = fopen(srcFile, "rb");
if (!fileInput)
{
printf("error opening fileInput file\n");
iRet = -1;
goto exit;
}
fileOutput = fopen(dstFile, "wb");
if (!fileOutput)
{
printf("error opening fileOutput file\n");
iRet = -1;
goto exit;
}
bzInput = BZ2_bzReadOpen(NULL, fileInput, 0, 0, NULL, 0);
if (!bzInput)
{
printf("error read open file\n");
iRet = -1;
goto exit;
}
while (1)
{
iBytesRead = BZ2_bzRead(&iResult, bzInput, acBuffer, sizeof(acBuffer));
if (iBytesRead == 0)
{
printf("bz2 read successful\n");
break;
}
if (iResult != BZ_OK && iResult != BZ_STREAM_END)
{
printf("error reading bz2 data:%d\n", iResult);
break;
}
fwrite(acBuffer, 1, iBytesRead, fileOutput);
}
BZ2_bzReadClose(&iResult, bzInput);
if (iResult != BZ_OK)
{
iRet = -1;
goto exit;
}
printf("decompress successful. written to %s\n", dstFile);
iRet = 0;
exit:
if (fileInput)
{
fclose(fileInput);
fileInput = NULL;
}
if (fileOutput)
{
fclose(fileOutput);
fileOutput = NULL;
}
return iRet;
}
int syncTimezone(const char *infoFile, const char *region)
{
std::string strRegionPach;
strRegionPach.append(infoFile);
strRegionPach.append("/");
strRegionPach.append(region);
FILE* file = fopen(strRegionPach.c_str(), "r");
if (file == NULL)
{
printf("open failed: %s\n", strRegionPach.c_str());
return -1;
}
printf("open success: %s\n", strRegionPach.c_str());
char last_line[512] = {0};
while (fgets(last_line, sizeof(last_line), file) != NULL) {
printf("xxxxx line: %s\n", last_line);
// 什么都不做,只需保留最后一行
}
fclose(file);
printf("last line: %s\n", last_line);
//uci set system.@system[0].timezone='AEST-10AEDT,M10.1.0,M4.1.0/3'
std::string strValue;
linuxPopenExecCmd(strValue, "uci set system.@system[0].timezone=%s", last_line);
linuxPopenExecCmd(strValue, "uci commit system");
linuxPopenExecCmd(strValue, "/etc/init.d/system restart");
return 0;
}
int main(int argc, char** argv)
{
int iRet = 0;
const char *inputFile = "/usr/share/zoneinfo_data.tar.bz2";
const char *deCompressFile = "/tmp/zoneinfo_data.tar";
const char *outputFile = "/tmp/zoneinfo_data";
iRet = deCompress(inputFile, deCompressFile);
if (iRet < 0)
{
printf("decompress failed\n");
return -1;
}
std::string strValue;
linuxPopenExecCmd(strValue, "tar -vxf %s", deCompressFile);
// syncTimezone(outputFile, "Australia/Canberra");
syncTimezone(outputFile, "Asia/Shanghai");
linuxPopenExecCmd(strValue, "rm -rf %s", deCompressFile);
linuxPopenExecCmd(strValue, "rm -rf %s", outputFile);
return 0;
}
这里我用的是bz2压缩,大家根据实际情况选择,替换代码即可,如果是glibc等使用/etc/localtime方式的话则最后是代码实现建立软连接到 /etc/localtime,例如ln -s /tmp/zoneinfo_data/zoneinfo/Australia/Canberra /etc/localtime