自动驾驶的关键路径如下,传感器的数据发送给感知模块;感知模块根据传感器数据来确定车辆所处的环境,比如前方有没有障碍物,是不是和车道线保持着适当的距离等;感知处理之后的数据传递给规控模块,规控根据车辆当前所处的环境来规划车辆的路线和加减速等;最后规控的结果要发送到底盘/动力来做真正的执行。
在自动驾驶的关键路径中,对确定性要求是非常高的,因为车辆是一个安全产品,一旦某个环节消耗的时间不符合确定性的要求,那么会造成比较大的影响。比如车辆前方有行人,那么车辆就需要及时刹停,不可延误。
确定性,考虑的是最恶劣的情况,假如规控模块要求,每次处理规控任务的处理时间不能超过2ms,那么就是要求无论系统运行在什么环境下,当前系统的负载是怎么样的,规控任务的处理时间都不能超过2ms。也就是说如果车辆连续运行了一周,假如规控运行的次数是1000万次,那么也不允许有一次超过2ms的。
1获取线程实际消耗的cpu时间
如下代码,如果要获取planning函数执行消耗的时间。在相当长的一段时间,都是直接获取CLOCK_BOOTTIME这种clock id的时间,这种时间都是墙上时间。在很多时候用这种时间来表示任务消耗的也是没有问题的,但是如果进程中发生了睡眠,发生了阻塞,使用这种时间就不准确了,这种时间不能表示任务实际消耗的cpu时间。
通过函数pthread_getcpuclockid获取的clock,表示cpu时间,也就是线程实际占用的cpu的时间。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>void planning() {int counter = 0;for(int i = 0; i < 10000; i++) {counter++;}for (int i = 0; i < 5; i++) {sleep(1);}
}int main() {clockid_t clock_id;struct timespec start_time1;struct timespec end_time1;struct timespec start_time2;struct timespec end_time2;pthread_getcpuclockid(pthread_self(), &clock_id);clock_gettime(clock_id, &start_time1);clock_gettime(CLOCK_BOOTTIME, &start_time2);planning();clock_gettime(clock_id, &end_time1);clock_gettime(CLOCK_BOOTTIME, &end_time2);printf("real time:%ldns\n", (end_time1.tv_sec * 1000 * 1000 * 1000 + end_time1.tv_nsec) - (start_time1.tv_sec * 1000 * 1000 * 1000 + start_time1.tv_nsec));printf("wall time:%ldns\n", (end_time2.tv_sec * 1000 * 1000 * 1000 + end_time2.tv_nsec) - (start_time2.tv_sec * 1000 * 1000 * 1000 + start_time2.tv_nsec));return 0;
}
运行结果如下,cpu clock id显示的时间是线程实际消耗的时间,是514微秒左右;wall time是5秒。
2获取线程调度次数
在linux中进程的status文件中显示了线程调度的次数。最后两行表示线程调度次数,voluntary_ctxt_switches表示线程主动调度的次数,比如当线程睡眠,IO阻塞时,会触发线程调度,这时的调度就是自愿调度;nonvoluntary_ctxt_switches表示线程非自愿调度的次数,比如当线程的时间片用完,被调度器强制调度,这种情况就是非自愿调度。
可以通过该文件获取调度次数,可以在调用planning之前获取线程的调度次数,返回之后再次获取调度次数,两者的调度次数差就基本上能表示在planning执行过程中发生的调度次数。这里之所以说是基本上,而不是绝对,因为在获取调度次数到planning真正被执行,以及planning返回到获取调度次数之间,也有可能发生调度。
root@wangyanlong-virtual-machine:/home/wangyanlong/test# cat /proc/12744/status
Name: a.out
Umask: 0022
State: S (sleeping)
Tgid: 12744
Ngid: 0
Pid: 12744
PPid: 2374
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 256
Groups: 0 999
NStgid: 12744
NSpid: 12744
NSpgid: 12744
NSsid: 2294
VmPeak: 2712 kB
VmSize: 2644 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 1024 kB
VmRSS: 1024 kB
RssAnon: 0 kB
RssFile: 1024 kB
RssShmem: 0 kB
VmData: 92 kB
VmStk: 132 kB
VmExe: 4 kB
VmLib: 1796 kB
VmPTE: 36 kB
VmSwap: 0 kB
HugetlbPages: 0 kB
CoreDumping: 0
THP_enabled: 1
Threads: 1
SigQ: 0/15188
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
Seccomp_filters: 0
Speculation_Store_Bypass: thread vulnerable
SpeculationIndirectBranch: conditional enabled
Cpus_allowed: ffffffff,ffffffff,ffffffff,ffffffff
Cpus_allowed_list: 0-127
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 42
nonvoluntary_ctxt_switches: 0
root@wangyanlong-virtual-machine:/home/wangyanlong/test#
有时候,当我们统计出来wall time比较大的时候,就说这个任务消耗的时间多,这样的说服力是比较弱的。这个时候我们就需要获取线程实际消耗的时间结合调度次数,来进行分析。
(1)如果real time和wall time都比较大,那么说明就是任务执行实际消耗的时间长,需要对任务本身的逻辑进行优化。
(2)如果real time比较小,wall time比较大,那么可能有两种情况
①在任务执行期间发生了调度,这个时候就需要通过绑核或者提高线程的优先级等方式来保证在任务执行期间不会发生调度。
②任务中存在阻塞的操作,等待一个条件满足,比如等待一个IO条件,等待一个mutex等,要结合代码进一步分析。