Boost教程(四)atomic无锁编程

atomic 无锁编程

多个线程之间共享地址空间,所以多个线程共享进程中的全局变量和堆,都可以对全局变量和堆上的数据进行读写,但是如果两个线程同时修改同一个数据,可能造成某线程的修改丢失;如果一个线程写的同时,另一个线程去读该数据时可能会读到写了一半的数据。这些行为都是线程不安全的行为,会造成程序运行逻辑出现错误。下面的程序很常见:

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
// boost::atomic<int> i(0);
int i=0;
boost::mutex mut;
void thread_1(int n);

int main()
{
int n=100000; // n不够大时,i不容易出现不同的情况
boost::thread th1 = boost::thread(boost::bind(&thread_1,n));
boost::thread th2 = boost::thread(boost::bind(&thread_1,n));
th1.join();
th2.join();
cout<< i<<endl;
return 0;
}

void thread_1(int n)
{
while(n--)
{
mut.lock();
i++;
mut.unlock();
}
}

如果不加lock,i最终值是不确定的,因为两个线程同时对i进行了写操作。一般地,我们用互斥锁mutex保护临界区,保证同一时间只能有一个线程可以获取锁,持有锁的线程可以对共享变量进行修改,修改完毕后释放锁,而不持有锁的线程阻塞等待直到获取到锁,然后才能对共享变量进行修改,最后i必定为200000

Boost提供了原子类型atomic,通过使用原子类型可摆脱每次对共享变量进行操作都进行的加锁解锁动作,节省了系统开销,同时避免了线程因阻塞而频繁的切换。atomic封装了不同计算机硬件的底层操作原语,提供了跨平台的原子操作功能,解决并发竞争读写变量的困扰,只需要包含文件<boost/atomic.hpp>,在上面的代码中使用boost::atomic<int> i(0);,然后去掉函数中的互斥锁,运行效果是一样的,而且节省了系统开销。

atomic可以把对类型T的操作原子化,T的要求:

  1. 标量类型(算数,枚举,指针)
  2. POD类型,可以使用memcmp,memset等函数

两种方式创建atomic对象:

1
2
3
4
5
6
atomic<int> a(10);
assert(a==10); //安全函数,若表达式不成立结束程序


atomic<long> L;
cout << L <<endl; //初始值不确定

最重要的两个成员函数: store() (operator=) 和 load() (operator T() )以原子方式存取,不会因为并发访问导致数据不一致。

1
2
3
4
5
6
7
8
9
boost::atomic<bool> b(1);
assert(b != 0);
std::cout << b << std::endl;
b.store(0);//存值
std::cout << b << std::endl;

boost::atomic<int> n1(100);
std::cout << n1.exchange(200) << std::endl; //交换两个值,并且返回原值100
std::cout << n1 << std::endl;

测试代码中临界区非常短,只有一个语句,所以显得加锁解锁操作对程序性能影响很大,但在实际应用中,我们的临界区一般不会这么短,临界区越长,加锁和解锁操作的性能损耗越微小,无锁编程和有锁编程之间的性能差距也就越微小。

无锁编程最大的优势在于两点:

  • 避免了死锁的产生。由于无锁编程避免了使用锁,所以也就不会出现并发编程中最让人头疼的死锁问题,对于提高程序健壮性有很大积极意义
  • 代码更加清晰与简洁

参考:C++11多线程编程


容器常用的工具函数

std::begin() 和 std::end()

std::begin()std::end()是STL中的函数模板,用于获取容器(数组、std::initializer_list、STL标准容器、std::string_view、std::array等)的起始和结束迭代器。它们提供了一种通用的方式来访问这些序列的边界,而不依赖于具体的容器类型。一般结合STL的算法一起使用

1
2
int a[] = { 1, 3, 5, 2, 9, 6, 8 };
std::sort(std::begin(a), std::end(a) );

感觉和现有的begin,end迭代器有些冗余

std::find

find() 函数是一个泛型算法,可以用于操作所有STL容器。它用于在数组或标准库容器(如vector, map)中查找指定元素,查找成功则返回一个指向指定元素的迭代器,查找失败则返回end迭代器。注意: 不是返回 true or false

时间复杂度是O(n)

1
2
3
4
5
6
7
8
9
10
vector<int> num_list = { 2,4,6,8,10,12 };
int find_num = 8; //要查找的元素,类型要与vector中的元素类型一致
std::vector<int>::iterator num = std::find(num_list.begin(), num_list.end(), find_num); //返回一个迭代器指针

if (num != num_list.end()) //查找成功
{
cout << "其索引为 " << distance(num_list.begin(), num) << endl;
}
else
cout<< "元素 " << find_num << " 不在num_list中" << endl;

注意区分map::findset::find,由于map和set内部是红黑树实现的。因此map和set内部的find函数查找时间复杂度是 O(logn)

copy

std::copy(start, end, container.begin());

copy只负责复制,不负责申请空间,所以复制前必须有足够的空间。如果container的大小小于输入序列的长度N的话,这段代码会导致崩溃(crash)。所以此时引入了back_inserter

1
std::copy(src.begin(), src.end(), std::back_inserter(dest));

标准库提供的back_inserter模板函数很方便,因为它为container返回一个back_insert_iterator迭代器,这样,复制的元素都被追加到container的末尾了。(就算container为空也没事)。

1
2
3
4
5
6
7
8
9
10
11
vector<int>  vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);

vector<int> vv;
std::copy(vec.begin(), vec.end(), std::back_inserter(vv) );
for(const auto &it: vv)
cout << it << " ";

fill

std::fill 是C++标准库中的一个函数,和copy相对。定义在 头文件中。它将指定的值赋给一个给定范围内的所有元素,常用于数据结构初始化,可以用于vector, list和数组。

1
2
std::vector<int> vec = {1, 2, 3, 4, 5};
std::fill(vec.begin(), vec.end(), 0); // 将 vec 中的所有元素设置为 0

使用之前,必须对vector分配内存,比如调用resize函数。

std::fill有很多重载形式,但最常用的是根据容器迭代器赋值的形式。

std::distance

用于计算两个迭代器之间的距离。它提供了一种方便的方法处理迭代器范围时进行遍历和计算。它在头文件<iterator>中定义,函数原型如下:

1
2
3
template< class InputIt >
typename std::iterator_traits<InputIt>::difference_type
distance( InputIt first, InputIt last );

1
2
std::vector<int> v{3, 1, 4, 1, 5, 9};
std::cout << "distance: " << std::distance(v.begin(), v.end()) << std::endl; // 6

std::distance()函数计算的是迭代器之间的距离,而不是元素的个数。因此,在使用时需要确保迭代器范围有效,否则可能导致未定义行为。
对于顺序容器(如向量、列表等),std::distance的时间复杂度为O(N),其中N是迭代器范围内的元素数量。对于随机访问迭代器(如指针、数组等),时间复杂度为O(1)

reverse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <vector>
#include <iostream>
#include <iterator>
#include <algorithm>

int main()
{
std::vector<int> v({1,2,3});
std::reverse(std::begin(v), std::end(v));
std::cout << v[0] << v[1] << v[2] << '\n';

int a[] = {4, 5, 6, 7};
std::reverse(std::begin(a), std::end(a));
std::cout << a[0] << a[1] << a[2] << a[3] << '\n';
}

cmp函数

cmp 函数的特点:

  1. 返回值为 bool 类型,用来表示当前的排序是否正确
  2. 参数为两个相同类型的变量,且类型与要排序的容器模板类型相同
    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
    // 从小到大排列
    bool cmp( const Data& d1, const Data& d2)
    {
    // return(d1.dist < d2.dist);
    // 由于重载了 <= ,可以直接这么写
    return(d1 <= d2);
    }

    vector<Data> vec;
    Data temp;
    for ( int i = 0; i < 8; ++i )
    {
    temp.dist = rand() %50;
    temp.confidence = 100;
    vec.push_back( temp );
    }
    for(int i=0; i<8; i++)
    {
    cout << vec.at(i).dist << " ";
    }
    cout << endl;
    // 第三个参数可以是一个函数指针,一般使用cmp函数
    stable_sort( vec.begin(), vec.end(), cmp );
    for(int i=0; i<8; i++)
    {
    cout << vec.at(i).dist << " ";
    }

std::accumulate 函数

用来计算特定范围内(包括连续的部分和初始值)所有元素的和,除此之外,还可以用指定的二进制操作来计算特定范围内的元素结果,需要 #include <numeric>。 三个形参:头两个形参指定要累加的元素范围,第三个形参则是累加的初值。

1
2
3
4
5
6
7
8
9
10
11
12
13
vector<int>  vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);

for(const auto &it: vec)
{
cout << it << " ";
}
cout << endl;
int num = std::accumulate(vec.begin(), vec.end(), 0, [](int input){return 2*input; } );

我们期待的结果是num为30,但是这样会报错 candidate: main()::<lambda(int)> int num = std::accumulate(vec.begin(), vec.end(), 0, [](int input){return 2*input; } ); ^ candidate expects 1 argument, 2 provided

最后一句应该改为 int num = std::accumulate(vec.begin(), vec.end(), 0, [](int n, int input){return n + 2*input; } );,得到num为30

sort

sort使用快速排序的递归形式,时间复杂度是O(nlog(n) )

Sort函数有三个参数:(第三个参数可不写)

  1. 第一个是要排序的数组的起始地址。
  2. 第二个是结束的地址(最后一位要排序的地址)
  3. 第三个参数是排序的方法,可以是从大到小也可是从小到大,还可以不写第三个参数,此时默认的排序方法是从小到大排序。
1
2
3
int b = 3, c = 12;
qDebug() << std::greater<int>()(b, c);
qDebug() << std::less<int>()(b, c);
1
2
3
4
5
6
7
8
int a[]={3,4,1,12,0,8,4};
// 默认从小到大排列
std::sort(a, a+7);
// 从大到小
std::sort(a, a+7, std::greater<int>());
for(int i=0; i < 7; i++)
cout << a[i] <<" ";
// 0 1 3 4 4 8 12

使用Lambda表达式作为排序标准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vector<int>  vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);

for(const auto &it: vec)
{
cout << it << " ";
}
cout << endl;
std::sort(vec.begin(), vec.end(),
[](int a, int b){
return (a%2) < (b%2);
}
);
for(auto it : vec)
{
cout << it << " ";
}

运行结果:
1
2
1  2  3  4  5  
2 4 1 3 5


针对自定义的数据类型,需要自定义排序的方式

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
struct Frontier
{
Frontier(double c):
cost(c){}

double cost;
};

bool compareFrontier(Frontier f1, Frontier f2)
{
return (f1.cost < f2.cost);
}
void showFrontiers(const vector<Frontier>& v)
{
for(auto f: v)
cout << f.cost << " ";
cout << endl;
}

vector<Frontier> frontiers;
frontiers.push_back(Frontier(1.2) );
frontiers.push_back(Frontier(4.9) );
frontiers.push_back(Frontier(12.7) );
frontiers.push_back(Frontier(0.3) );
frontiers.push_back(Frontier(3.6) );
showFrontiers(frontiers);

std::sort(frontiers.begin(), frontiers.end(),
[](const Frontier& f1, const Frontier& f2) { return f1.cost < f2.cost; });
// std::sort(frontiers.begin(), frontiers.end(), compareFrontier);

showFrontiers(frontiers);

frontiers一般用vector或数组,不能是list, dequeue, queue

运行结果:

1
2
1.2  4.9  12.7  0.3  3.6  
0.3 1.2 3.6 4.9 12.7


ROS2的概述和安装

ros2.0是一个跨平台的机器人开发框架,它在ros1.0的基础上进行了重构和改进,以适应更多的应用场景和需求。ros2.0的架构可以分为以下几个层次:

  • OS层: ros2.0支持多种操作系统,包括Linux、Windows、macOS、RTOS等,也支持没有操作系统的裸机.。

  • 中间层: ros2.0采用基于RTSP协议的DDS (Data-Distribution Service) 作为中间层,DDS是一种用于实时和嵌入式系统发布-订阅式通信的工业标准,它提供了点对点的通信模式,不需要像ros1.0那样借由master节点来完成两个节点间通信,这使得系统更加容错和灵活。

  • 接口层: ros2.0提供了两个主要的接口层,分别是rmw (ros middleware interface) 和rd (ros dient libraries) 。rmw是相对底层的接口层,直接和DDS交互,C语言实现:rc是对mw相对高层的抽象,C/C++实现。此外,还有一个ros to dds组件,主要为用户直接访问DDS层提供接口。

  • 应用层: ros2.0支持用C++或者Python来编写应用程序,也支持其他语言的绑定。应用程序可以通过r或者ros to dds来调用中间层的功能。

如果项目需要实时性和分布通信支持,ROS 2.0提供了更好的解决方案

安装

安装参考 安装ROS2的过程,唯一不同地方是我没有遇到 2.3 中的报错。

截至目前有两个ROS 2的版本还没有到项目终止日期 (EOL end-of-life),一个是最新的发布版本为Iron Irwini,发布时间是2023年5月23日,EOL date是2024年11月,另一个是Humble Hawksbil,发布时间是2022年5月23日,EOL date是到2027年5月,比较后我选择了Humble这个版本来学习。

ament_cmake是cmake的增强版

ROS2安装指定包,和ROS1一样,例如 sudo apt-get install ros-humble-irobot-create-msgs


ubuntu配置CUDA
  • Ensure there is enough space in /tmp and that the installation package is not corrupt

刚开始安装./cuda_12.1.1_530.30.02_linux.run时, /所在的/dev/sdb7所剩空间越来越小,最终安装失败,提示 Ensure there is enough space in /tmp and that the installation package is not corrupt。 所以必须先扩展/所在的硬盘容量

按这个步骤安装,一般不会出问题: ubuntu18.04上cuda及cudnn安装


交叉编译 PCL

在x86架构的Ubuntu22.04交叉编译PCL源码,以能在ARM aarch64系统上运行,也就是全志MR527的TinaLinux 5.15.123 aarch64

先看完成的状态,也就是ARM平台的PCL库文件链接关系:

libpcl_kdtree.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ldd  /usr/lib/aarch64-linux-gnu/libpcl_kdtree.so
linux-vdso.so.1 (0x0000ffff8d240000)

libpcl_common.so.1.10 => /lib/aarch64-linux-gnu/libpcl_common.so.1.10 (0x0000ffff8cfb7000)
liblz4.so.1 => /lib/aarch64-linux-gnu/liblz4.so.1 (0x0000ffff8cf89000)


libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff8cda4000)
libgomp.so.1 => /lib/aarch64-linux-gnu/libgomp.so.1 (0x0000ffff8cd56000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff8cd32000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff8cbbf000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff8d210000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff8cb14000)
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff8cae3000)
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 (0x0000ffff8cacf000)

libpcl_common.so

1
2
3
4
5
6
7
8
9
10
ldd  /usr/lib/aarch64-linux-gnu/libpcl_common.so
linux-vdso.so.1 (0x0000ffff975e8000)
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff972d0000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff97225000)
libgomp.so.1 => /lib/aarch64-linux-gnu/libgomp.so.1 (0x0000ffff971d7000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff971b3000)
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff97182000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff9700f000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff975b8000)
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2


交叉编译 PCL

在终端找到/opt/cmake/bin,启动cmake-gui,这里是新安装的cmake 3.27. 选择PCL的源码目录和build目录,选择交叉编译平台。没有显示需求,所以cmake-gui不需要选择 OpenGL和VTK

The C compiler identification is unknown

既然是交叉编译,那就得用跨平台的编译器,注意在启动cmake-gui的终端要提前设置环境变量:

1
2
3
4
5
export ARCH=arm64
export TOOLCHAIN_PATH=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu
export LD_LIBRARY_PATH=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
export CROSS_COMPILE=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-

不重要的报警

configure PCL时出现的部分信息,可以无视

1
2
3
4
Could NOT find PkgConfig (missing: PKG_CONFIG_EXECUTABLE)                 # OMIT
Could NOT find LIBUSB_1 (missing: LIBUSB_1_LIBRARY LIBUSB_1_INCLUDE_DIR) # OMIT
Could NOT find ZLIB (missing: ZLIB_LIBRARY ZLIB_INCLUDE_DIR)
Could NOT find PNG (missing: PNG_LIBRARY PNG_PNG_INCLUDE_DIR)

编译的过程中,发现需要处理Boost, Flann, lz4, hdf5等库,它们也需要交叉编译。

交叉编译Boost

PCL需要Boost,cmake时会提示无法找到Boost的错误,不是本机的x86 Boost,是aarch64的Boost,也就是说需要交叉编译Boost。

下载Boost1.74源码,找到bootstrap.sh,然后执行

1
./bootstrap.sh  --with-libraries=system,filesystem,thread,date_time,iostreams --with-toolset=gcc 

修改project-config.jam文件,把using gcc部分修改为
using gcc : arm : /home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc ;

找到gcc.jam文件,在430行左右,增加一行

1
2
3
4
5
6
# On Windows, fPIC is the default, and specifying -fPIC explicitly leads
# to a warning.
local non-windows = [ set.difference $(all-os) : cygwin windows ] ;
compile-link-flags <link>shared/<target-os>$(non-windows) : -fPIC ;
# 增加下面这行
compile-link-flags <link>static/<target-os>$(non-windows) : -fPIC ;

此步骤的主要目的是打开-fPIC,避免PCL在编译时找不到boost库的.a文件

进行编译并安装boost:

1
sudo ./b2 cxxflags=-fPIC cflags=-fPIC -a install

编译时可能会报错: error while loading shared libraries: libisl.so.22: cannot open shared object file: No such file

将交叉编译的toolschain地址的这几个库文件都复制到/usr/lib/x86_64-linux-gnu: libctf-arm64.so.0, libopcodes-2.34-arm64.so, libbfd-2.34-arm64.so, libisl.so.22。 如果没报错就不用了。

编译结束,我把生成的Boost库的文件都放到了/usr/local/lib。也就是/usr/lib/x86_64-linux-gnu中的libboost开头的库文件仍是x86的,/usr/local/lib中的libboost开头的库文件是AArch64架构。对于ARM的so库文件,在x86上用ldd命令查看会出现 not a dynamic executable

修改PCL中的pcl_find_boost.cmake文件

1
2
3
4
set(Boost_DIR  "/usr/lib/x86_64-linux-gnu/cmake")
# Required boost modules
set(BOOST_REQUIRED_MODULES filesystem date_time iostreams system)
find_package(Boost 1.74.0 REQUIRED COMPONENTS ${BOOST_REQUIRED_MODULES})

lz4 1.9.1 arm64版本的交叉编译

下载源码后执行

1
2
3
make CC=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc
mkdir lz4_arm
make install PREFIX=$(pwd)/lz4_arm

运行ldconfig, 出现 /sbin/ldconfig.real: /lib/x86_64-linux-gnu/liblz4.so.1 is not a symbolic link

运行file /lib/liblz4.so,出现liblz4.so: broken symbolic link to liblz4.s

目前解决: 把地平线x3M的 /lib/liblz4.so/lib/liblz4.so.1 复制到我的本机的 /lib目录, /usr/lib/x86_64-linux-gnu的几个 liblz4.so 仍然是 x86 平台。

hdf5的问题

编译flann时,链接hdf5出错

1
2
3
/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/../lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld:   /usr/lib/x86_64-linux-gnu/hdf5/serial/libhdf5.so:   error adding symbols: file in wrong format

collect2: error: ld returned 1 exit status

显然需要的是ARM的hdf5,这个库的交叉编译特别麻烦,最后解决方法是从一台ARM平台的主机上找到 hdf5文件夹,放到了 /usr/lib/x86_64-linux-gnu

如果交叉编译hdf5,参考:

  1. 使用cmake-gui进行configure和generate,第一次configure会失败,重新configure即可
  2. 进行make,两次,编译失败
  3. 拷贝bin/H5detect bin/H5make_libsettings libhdf5.settings到arm平台
  4. 在arm平台修改文件执行权限,执行H5detect和H5make_libsettings,把程序输出分别保存到H5Tinit.c和H5lib_settings.c
  5. 拷贝H5Tinit.c和H5lib_settings.c到主机的编译目录下,继续编译

交叉编译flann库

在一台ARM平台的设备上,flann库文件是这样的

1
2
3
4
5
6
7
8
9
/usr/lib/aarch64-linux-gnu/libflann.so
/usr/lib/aarch64-linux-gnu/libflann.so.1.9
/usr/lib/aarch64-linux-gnu/libflann.so.1.9.1

/usr/lib/aarch64-linux-gnu/libflann_cpp.so
/usr/lib/aarch64-linux-gnu/libflann_cpp.so.1.9
/usr/lib/aarch64-linux-gnu/libflann_cpp.so.1.9.1
/usr/lib/aarch64-linux-gnu/libflann_cpp_s.a
/usr/lib/aarch64-linux-gnu/libflann_s.a

链接关系:
libflann.so
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
ldd /usr/lib/aarch64-linux-gnu/libflann.so
linux-vdso.so.1 (0x0000ffff85887000)
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff8511b000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff85070000)
# 缺
libgomp.so.1 => /lib/aarch64-linux-gnu/libgomp.so.1 (0x0000ffff85022000)

libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff84ffe000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff84e8b000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff85857000)
# 目前缺这两个
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 (0x0000ffff84e77000)
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff84e46000)


ldd /usr/lib/aarch64-linux-gnu/libflann_cpp.so
linux-vdso.so.1 (0x0000ffff9fb1f000)
# 目前需要交叉编译
liblz4.so.1 => /lib/aarch64-linux-gnu/liblz4.so.1 (0x0000ffff9fa68000)
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff9f883000)
# 目前缺
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff9f852000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff9f6df000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff9faef000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff9f634000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1

如果之前缺hdf5,编译会报警 hdf5 library not found, not compiling flann_example.cpp

解决方法: 把ARM平台的的flann库文件复制到了 /usr/lib/x86_64-linux-gnu,原来的x86的flann库文件复制到了新文件夹 /usr/lib/x86_64-linux-gnu/x86_flann

如果要交叉编译,需要修改CMakeLists:

1
2
3
4
5
pkg_check_modules(LZ4 REQUIRED liblz4)
#include_directories(${LZ4_INCLUDE_DIRS})
include_directories(/home/user/Downloads/lz4-1.9.1/lz4_arm/include)

link_directories(/home/user/Downloads/lz4-1.9.1/lz4_arm/lib)


现在继续使用cmake-gui交叉编译PCL,在界面中没有选择所有模块,执行ConfigureGenerate后,会有日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
编译的模块:
common kdtree octree search
sample_consensus filters 2d
geometry io features ml
segmentation surface keypoints
tracking stereo tools

不会编译的模块:
visualization: VTK was not found.
registration: Disabled manually.
recognition: Requires registration.
apps: Disabled: registration missing.
outofcore: Requires visualization.
examples: Code examples are disabled by default.
people: Requires visualization.
simulation: Disabled: visualization missing.
global_tests: No reason
tools: Disabled: registration missing.

然后去build文件夹找每个模块的Makefile,比如pcl-pcl-1.10.0/build/kdtree,然后执行make即可。

PCL交叉编译得到的so文件特别大,有的到了800M。使用strip瘦身,结果报错 strip: Unable to recognise the format of the input file ,因为so文件是aarch64的,应当使用gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/aarch64-linux-gnu/bin/strip,结果让libpcl_features.so.1.10.0从797M 下降到 39M。

所有编译生成的库文件在pcl-pcl-1.10.0/build/lib,把这些库文件放到开发板上,比如路径/pcl_libs,所有Boost的库文件也放过去,比如/boost_libs


测试程序

在本机上写一个PCL的测试程序,CMake部分:

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
cmake_minimum_required(VERSION 3.5)

project(test_cross_pcl LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Boost COMPONENTS system filesystem thread REQUIRED)

include_directories(
include
/usr/include/eigen3

/home/user/Downloads/pcl-pcl-1.10.0/common/include
/home/user/Downloads/pcl-pcl-1.10.0/build/include
/home/user/Downloads/pcl-pcl-1.10.0/io/include
/home/user/Downloads/pcl-pcl-1.10.0/filters/include
)
LINK_DIRECTORIES(/home/user/Downloads/pcl-pcl-1.10.0/build/lib)
LINK_DIRECTORIES(/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/aarch64-linux-gnu/lib)

add_executable(test_cross_pcl main.cpp)
target_link_libraries(test_cross_pcl
-lpcl_common
-lpcl_io
-lpcl_filters
-lpthread
)

不能使用本机上的编译器,还用cmake-gui进行交叉编译: configure, generate, 然后到对应的build文件夹里执行make,把生成的可执行文件放到开发板。

执行export LD_LIBRARY_PATH=/pcl_libs:/boost_libs:$LD_LIBRARY_PATH,也就是指定链接库的地址,再执行就成功了。

参考:Windows 10上源码编译PCL 1.8.1支持VTK和QT,可视化三维点云
交叉编译Boost

解决vcpkg无法交叉编译arm64版本 HDF5 库的问题
编译HDF5
Linux安装HDF5及遇到的问题总结

FLANN 1.9.2 源码编译
PCL与VTK


全局路径远离障碍物
abstract Welcome to my blog, enter password to read.
Read more
constexpr

修饰变量的时候,可以把 constexpr 对象当作加强版的 const 对象。const 对象值不会改变,但不一定能够在编译期取得结果;constexpr 对象不仅值不会改变,而且保证能够在编译期取得结果。如果一个 const 变量能够在编译期求值,将其改为 constexpr 能够让代码更清晰易读。

constexpr 函数可以把运行期计算迁移至编译期,使得程序运行更快(但会增加编译时间)。但如果 constexpr 函数中存在无法在编译期求值的参数,则 constexpr 函数和普通一样在运行时求值,此时的返回值不是常量表达式。

尽管编译期运算会延长我们的编译时间,但是我们有些时候会用它来加快程序的运行速度。但是在使用时我们仍应该抱着谨慎的态度。太多的constexpr让代码中充斥着不必要的关键字,影响可读性,而且要权衡编译期带来的好处和坏处。

目前我在函数中几乎没用过constexpr,只对头文件中常用的枚举值和变量加了constexpr,暂时不做太多分析了。

参考:


判断文件是否存在
1
2
3
4
5
6
7
8
#include <fstream>

std::ifstream fin(file_name);
if (!fin) {
printf("file not exist");
return false;
}
fin.close();

C++17中有std::filesystem::path,相当于Boost的filesystem模块,只需要set(CMAKE_CXX_STANDARD 17)

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
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;


int main()
{
std::filesystem::path test_path("/home/user/test.yaml");
//判断路径是否存在
std::cout << "is_exist = " << std::filesystem::exists(test_path) << std::endl;

// 判断路径为绝对路径还是相对路径
std::cout << "is_abs = " << test_path.is_absolute() << std::endl;
std::cout << "is_relative = " << test_path.is_relative() << std::endl;
// 取得不带扩展名的文件名
std::cout << "stem = " << test_path.stem() << std::endl;
// 取得扩展名,如果没有,则为空
std::cout << "extension = " << test_path.extension() << std::endl;
// 取得文件名
std::cout << "filename = " << test_path.filename() << std::endl;

std::cout << "----------------------------------------" << std::endl;

std::filesystem::path test_dir("/home/user/Pictures");
//判断是文件还是文件夹
std::cout << "is_file = " << std::filesystem::is_regular_file(test_path) << std::endl;
std::cout << "is_dir = " << std::filesystem::is_directory(test_dir) << std::endl;

//关于路径拼接
std::filesystem::path new_path = test_dir / "test.txt";
std::cout << "new_path = " << new_path << std::endl;

//取得当前绝对工作路径 编译后可执行文件的路径文件夹
std::filesystem::path workPath = std::filesystem::current_path();
std::cout << "current exe path = " << workPath << std::endl;

//递归遍历指定路径下所有文件,文件夹名字也会返回
std::filesystem::recursive_directory_iterator iterDir(test_dir);
for (auto &it: iterDir)
{
//打印绝对路径
std::cout << it << std::endl;
// 不打印路径而是打印文件名
std::cout << it.path().filename() << std::endl;
}

//给一个相对路径,返回绝对路径
std::filesystem::path path_test("./");
std::cout << "abs_path = " << std::filesystem::absolute(path_test) << std::endl;


// 删除文件, 路径不存在不报错
// std::filesystem::remove(test_dir / "deepfakes"/"yolov7.pdf");

// 拷贝文件夹, 删除文件夹
// std::filesystem::copy(test_dir, test_dir.parent_path() / "works", std::filesystem::copy_options::recursive);
// std::filesystem::remove_all(test_dir.parent_path() / "works");
return 0;
}


yaml-cpp的使用

使用yaml-cpp读文件

1
2
3
4
5
6
7
8
9
10
YAML::Node gps_root;
try {
gps_root = YAML::LoadFile("/home/user/111.yaml");
} catch (YAML::ParserException &ex) {
std::cerr << "gps.yaml parse failed: " << ex.what() << std::endl;
exit(-1);
} catch (YAML::BadFile &ex) {
std::cerr << "gps.yaml load failed: " << ex.what() << std::endl;
exit(-1);
}

所用函数的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Node LoadFile(const std::string& filename) {
std::ifstream fin(filename.c_str());
if (!fin) {
throw BadFile();
}
return Load(fin);
}

Node Load(std::istream& input)
{
Parser parser(input);
NodeBuilder builder;
if (!parser.HandleNextDocument(builder)) {
return Node();
}

return builder.Root();
}

如果读文件失败,会抛出异常,不会运行到Load,所以无法用IsDefined函数判断是否读文件成功。


无法到达目标时,搜索新的目标点
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
double planner::distance(double x1,double y1,double x2,double y2)
{
return sqrt( (x1-x2) * (x1-x2) + (y1-y2) * (y1-y2) );
}

bool planner::isAroundFree(unsigned int mx, unsigned int my)
{
if(mx <= 1 || my <= 1 || mx >= this->costmap_->getSizeInCellsX()-1 || my >= this->costmap_->getSizeInCellsY()-1)
return false;
int x,y;
for(int i=-1;i<=1;i++)
{
for(int j=-1;j<=1;j++)
{
x = static_cast<int>(mx) + i;
y = static_cast<int>(my) + j;
if(this->costmap_->getCost(static_cast<unsigned int>(x),static_cast<unsigned int>(y)) != costmap_2d::FREE_SPACE)
return false;
}
}
return true;
}

// tolerance:搜索半径,单位米。
// in:输入的点
// out:搜索到的可行点
void planner::getNearFreePoint(const geometry_msgs::PoseStamped in,
geometry_msgs::PoseStamped& out,
double tolerance)
{
out = in;
unsigned int grid_size = static_cast<unsigned int>(tolerance/costmap_->getResolution() + 0.5);
if(grid_size<1)
{
out = in;
return;
}

unsigned int mx0,my0;
if(costmap_->worldToMap(in.pose.position.x,in.pose.position.y,mx0,my0))
{
if(this->isAroundFree(mx0,my0))
return;
unsigned int minx,maxx,miny,maxy;
double wx = 0.0,wy = 0.0;
double min_move_cost = 10000000.0;
minx = mx0-grid_size>0?mx0-grid_size:0;
maxx = mx0+grid_size<costmap_->getSizeInCellsX()?mx0+grid_size:costmap_->getSizeInCellsX();
miny = my0-grid_size>0?my0-grid_size:0;
maxy = my0+grid_size<costmap_->getSizeInCellsY()?my0+grid_size:costmap_->getSizeInCellsY();
for(unsigned int i=minx;i<=maxx;i++)
{
for(unsigned int j=miny;j<=maxy;j++)
{
costmap_->mapToWorld(i,j,wx,wy);
double current_move_cost = this->distance(in.pose.position.x,in.pose.position.y,wx,wy);
if(!this->isAroundFree(i,j) || current_move_cost > tolerance)
continue;
if(min_move_cost > current_move_cost)
{
min_move_cost = current_move_cost;
out.pose.position.x = wx;
out.pose.position.y = wy;
}
}
}
}
}