首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

从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 的异常捕获,这里不再展开了。总的来说,这个函数就干了三件事情:

  1. 生成pid文件
  2. 分配uuid,启动日志系统
  3. 构建roslaunch_parent对象,并开启服务
这个 下面让我们来研究一下这个 roslaunch_parent 的对象。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1