从roscore开始
1. 解析 roscore 脚本
我们在终端里运行的roscore
实际上是在"/opt/ros/noetic/bin"目录下的一个python脚本。
这里是官方托管在github上的源码。
其内容十分简单,先一顿 import,加载了各种依赖库:
import sys
from optparse import OptionParser
from rosmaster.master_api import NUM_WORKERS
from roslaunch.nodeprocess import DEFAULT_TIMEOUT_SIGINT, DEFAULT_TIMEOUT_SIGTERM
然后定义了变量 NAME 和函数 _get_optparse():
NAME = 'roscore'
def _get_optparse():
parser = OptionParser(...)
# 省略函数的具体内容,大体意思就是添加各种命令行参数的配置项
return parser
接着,通过函数 _get_optparse() 解析命令行输入参数,将之区分为配置项(options)和参数项(args)。
parser = _get_optparse()
(options, args) = parser.parse_args(sys.argv[1:])
if len(args) > 0:
parser.error("roscore does not take arguments")
最关键的是下面的这两句,通过roslaunch实际开启roscore。
import roslaunch
roslaunch.main(['roscore', '--core'] + sys.argv[1:])
从这里我们也可以了解到,roscore 的启动本质还是通过 roslaunch 这个工具把主节点(Master)拉起来。 这也是很多人发现,在没有运行 roscore 的时候直接通过 roslaunch 启动各种应用也不会有问题,这一现象产生的原因。 所以 roscore 也只是包 roslaunch 中的一个脚本而已。
2. roslaunch.main
包 roslaunch 是 ROS 的通信技术栈 ros_comm 中的一个用 python 写的工具,它通过解析一个 xml 格式的文件,启动各个 ros 节点。它是我们日常开启机器人应用的主要方法。 这里我们研究从这个 roslaunch.main 到主节点成功运行并提供服务的整个过程。在我的电脑里,roslaunch 的各个python文件被安装在了 "/opt/ros/noetic/lib/python3/dist-packages/roslaunch" 这个目录下。
为了方便研究整个启动过程,我直接对该目录下的文件进行了修改,添加了一些日志来跟踪运行过程和各个变量。
首先,让我们过一遍刚刚调用的roslaunch.main
这个函数。
按照 python 的运行机制,上面第14行import roslaunch
的时候会先找到其安装目录下的
__init__.py文件,
完成模块的加载。
def main(argv=sys.argv):
options = None
logger = None
try:
from . import rlutil
parser = _get_optparse()
(options, args) = parser.parse_args(argv[1:])
args = rlutil.resolve_launch_arguments(args)
_validate_args(parser, options, args)
在该文件中实现了
main函数,
其代码片段如右侧所示,它只有一个参数 argv 缺省的情况下会用系统的传参。这里的 sys.argv 是模块 sys 的一个运行变量,
它是一个字符串的数组记录了运行程序时,由终端传递的各个参数,相当于C语言中main函数的输入参数argv。
模块 sys 提供了各种关于python运行环境的变量和函数。由于运行roscore的时候我们一般都没有在终端里输入什么参数,
所以这里的传参也只有['roscore', '--core']
这两个字符串而已。
在函数的一开始,定义了两个变量,options 用来记录输入参数 argv 中记录的各个可选项,logger 则是一个日志对象。
接着在一个 try-catch 语句块中构建了参数解析器 parser 并完成了相关的校验解析的工作。 经过修改代码各种输出日志之后发现,下面两个 if 的条件都为 false,故略。
if any([options.node_args, options.node_list, options.find_node, options.dump_params, options.file_list, options.ros_args]):
# 略。
if options.wait_for_master:
# 略。
然后调用函数 write_pid_file 把当前进程的pid写到一个文件里。这里传参options.pid_fn是指将要写入的文件名称, 实际传参是空的,将在函数内分配一个默认的文件,这里是"~/.ros/roscore-11311.pid"。 options.core传入时为true表示需要开启一个ros的主节点。options.port则指定了master的服务端口,缺省的情况下为11311。
write_pid_file(options.pid_fn, options.core, options.port)
def write_pid_file(options_pid_fn, options_core, port):
ros_home = rospkg.get_ros_home()
port = DEFAULT_MASTER_PORT
pid_fn = os.path.join(ros_home, 'roscore-%s.pid'%(port))
if not os.path.exists(ros_home):
os.makedirs(ros_home)
with open(pid_fn, "w") as f:
f.write(str(os.getpid()))
右侧是我根据输入参数对函数 write_pid_file 简化之后的代码,删除了一些不是那么重要的代码。
首先,通过 rospkg 的接口获得 ros 的 home 路径,在我的电脑上是 "~/.ros"。这个路径并不是我们平常写代码的工作空间, 它是用来保存ros系统运行过程中产生的一些数据、日志等文件的。write_pid_file 生成的文件也将保存在该目录下。
port 是主节点 ROS Master 的运行端口,主节点通过它来提供名字服务。这里使用默认值 11311。
确认了ros_home路径和工作端口之后,生成了pid文件命名 pid_fn。当ros_home所指路径不存在的时候,将新建一个。最后通过操作系统的接口获取当前进程的pid并写到文件中。
接着生成一个uuid,并启动日志系统。根据 rlutil 的接口 get_or_generate_uuid 中的注释说, 这个uuid有三个来源:其一,通过运行是传递的选项options.run_id指定, 其二,如果没有指定,并且当前系统已经有一个roscore运行了,就从参数服务器中获取,其三,如果是开启roscore就现场生成一个。对应我们的情况,是这里的第三个来源。
uuid = rlutil.get_or_generate_uuid(options.run_id, options.wait_for_master)
configure_logging(uuid)
最后,构建了一个 roslaunch_parent 的对象,并通过接口start和spin开启并持续运行服务。 这个对象实际开启了xmlrpc服务,构建了 ROS Master 的主节点进程,并且从一个core.launch的文件里运行了 rosout 的节点进程提供日志记录服务。
根据官方文档的介绍, 这个 roslaunch 能够以 client/server 的模式开启远程的进程(remote processes)。在本机上运行的 roslaunch 被称为是 parent,用来管理本机的进程。 roslaunch_parent 还会在远程机器上开启一个 child 进程,通过调用 child 进程的接口,就可以开启远程的 ros 节点了。
if options.child_name:
# 不执行,略。
else:
logger.info('starting in server mode')
# 省略一些与本文主题无关的代码
from . import parent as roslaunch_parent
if options.core:
options.port = options.port or DEFAULT_MASTER_PORT
p = roslaunch_parent.ROSLaunchParent(uuid, args, roslaunch_strs=roslaunch_strs,... # 还有好多参数略了)
p.start()
p.spin()
roslaunch.main剩下的代码都是 try-catch 的异常捕获,这里不再展开了。总的来说,这个函数就干了三件事情:
- 生成pid文件
- 分配uuid,启动日志系统
- 构建roslaunch_parent对象,并开启服务