Bash Shell
Shell是位于用户与操作系统内核之间的桥梁,当用户在终端敲入命令后,这些输入首先会进入内核中的tty子系统,TTY子系统负责捕获并处理终端的输入输出流,确保数据正确无误的在终端和系统内核之中。Shell在此过程不仅仅是一个监听者,还会积极地从tty子系统读取用户的命令输入,并对其进行解析来识别用户意图(比如当你输入ls
时,是Shell解析这个命令并调用/bin/ls
文件的来直接解析)。Bash Shell(基础 shell)是 Linux/Unix 系统中用户与操作系统内核交互的基本命令行界面,它是系统启动后为用户提供的默认命令解释器环境。
Bash和Shell其实是包含关系。Shell是命令行解释器的统称 ,有Bash、Zsh、Fish、Dash等多种命令行解释器,Bash是Shell的最流行方式,Ubuntu系统默认使用的也是Bash,当然你可以通过在终端中输入特定命令行可以切换(一般这几种命令行解释器的命令是兼容的,但是存在一些语法和交互功能的差异),一般使用Bash就足够了。
Bash Shell与终端不同点:Bash Shell是命令行解释器程序,终端则是输入输出设备/界面。可以理解为终端是"显示器+键盘",而Bash是运行在这个显示器上的"智能程序"。一般的执行流程是-------你在终端输入命令 → 终端将输入传给Shell → Shell解释执行 → 结果返回终端显示
交互式Shell与非交互式Shell
简单来说,交互式Shell用户可以 直接输入命令 并 实时看到输出 的 Shell,会显示 PS1
提示符(如 user@host:~$
),等待用户输入。比较常见的场景:1,直接打开终端 2,通过 SSH 登录后手动操作 (如 ssh [email protected]
后进入 Shell3,手动运行 bash -i
(强制交互模式)
非交互式Shell,不等待用户输入 ,直接执行 单条命令 后退出,所以说一般是看不到的,不会显示 PS1
提示符 ,执行完命令后立即关闭。比较常见的场景:1,通过 ssh user@host "command"
远程执行命令。2,使用 Python paramiko.exec_command("command")。3,
运行 Shell 脚本(如 bash script.sh
),如果输入bash -i script.sh会强制交互式
登录式Shell和非登录式Shell
登录式 Shell 的关键特征是:
-
以用户身份初始化完整的登录会话(加载登录配置文件,如
~/.bash_profile
)。 -
通常出现在系统首次登录时(如 SSH 登录、本地终端登录、
su - username
)。
.sh脚本文件
我们平常为了方便而写的脚本文件,实际上就是一个纯文本文件,包含一系列 Shell 命令 (如 Bash、Zsh 等支持的语法),将需要手动逐条输入的命令,预先整体写入文件,实现 自动化执行。比如,以下脚本文件(文件名demo.sh),Shell解析器就会一行一行的解析每一行(即每一条)内容。
bash
#!/bin/bash
echo "Hello World"
ls -l
date
执行的.sh文件的方式有以下几种
- bash demo.sh:显式指定解释器(bash),启动一个新的子 Shell 进程 来执行脚本。直接调用
/bin/bash
程序,将demo.sh
作为参数传递给它,忽略 Shebang 行(即首行的#!/bin/bash**)**。 - ./demo.sh:依赖脚本首行的 Shebang(如
#!/bin/bash
)。与bash的区别还有重要的一点就是bash demo.sh不需要赋予文件执行权限,而./demo.sh必须赋予文件执行权限 - source demo.sh(也可以简写为. demo.sh):不启动子Shell,直接在当前 Shell 进程中逐行执行脚本,并且会忽视Shebang。脚本中定义的变量、函数、别名等会影响当前 Shell。而前两种执行方式脚本中定义的变量、函数不会影响当前 Shell 环境。一般用于加载环境配置,比如
bash
# 加载环境配置(如 ~/.bashrc)
source ~/.bashrc
# 脚本中修改当前 Shell 的工作目录
source change_dir.sh # 脚本内容: cd /some/path
在交互式Shell和非交互式Shell说到过,bash 运行Shell脚本时会启动一个非交互式Shell,也就是启动一个新的子Shell来运行这个脚本,并且这个子Shell进程是非交互式的Shell,执行完毕后,控制权返回给父 Shell。而source不会启动子Shell,所以说它会在当前的交互式Shell运行。
.bashrc文件
.bashrc
文件是 Linux 系统中非常重要的配置文件 ,它与是否安装 ROS 无关,是 Bash shell的标准配置文件(它是一个纯文本文件,使用 Bash 语法 编写),"rc" 通常代表 "run commands" (运行命令)。文件名中的点(.)表示它是隐藏文件,在Linux系统用户主目录下,按ctrl+h键可以显示隐藏文件。
.bashrc的内容语法(完全遵循Bash语法):
- export PATH="$PATH:/my/custom/path" 变量定义。用来设置环境变量,影响所有子进程
- alias ll='ls -alF' 起别名,在终端输入ll就相当于输入ls -alF
- greet() { echo "Hello, $USER!"; } 函数定义
- if [ -d ~/projects ]; then
echo "Projects directory exists"
fi 条件循环判断
.bashrc如何运行?
当启动非登录的交互式 Bash shell 时,打开新的终端时,通过bash命令启动新的Shell时,.bashrc文件都会自动启动
必须需要注意的是在非交互式Shell中默认不会自动加载.bashrc ,也就是说你在使用bash执行脚本时在新打开的Shell子进程中不会加载.bashrc也就不会启动对应的环境(但是子进程会继承父进程的环境变量)。在交互式Shell中默认会加载.bashrc (因为 ~/.bash_profile
或 ~/.profile
通常会显式调用 .bashrc
)。登录式Shell会加载 ~/.bash_profile
或 ~/.profile,~/.bash_profile
或 ~/.profile
通常会显式调用 .bashrc所以.bashrc也会自动调用。
非登录式Shell不会加载配置文件。
其实在非交互式Shell不会自动加载.bashrc,这是因为.bashrc的开头有以下代码。这表示:仅当Shell是交互式时 ,才继续执行后续内容。非交互式Shell会直接return
退出,也就是说即使你在非交互式Shell单独调用source ~/.bashrc,也不会加载.bashrc的配置内容。这是为了避免用户自定义配置影响自动化任务而设置的,为了安全起见也不要试图去修改它!!!
XML
case $- in
*i*) ;;
*) return;;
esac
除此之外还有.bash_profile和.profile文件。.bashrc在非登录的交互式Bash Shell(不需要重新认证的子会话,如图形终端新建窗口、bash
命令启动等,直接继承父 Shell 的环境变量) 自动加载;.bash_profile**在Bash登录Shell(需要用户认证的完整会话,如系统登录、SSH 连接等,全新环境(会重新读取配置))**自动加载,也就是说.bash_profile登录时一次性设置。.profile是在所有Shell的登录会话都会加载,但是前提是.bash_profile不存在。也就是说只有在用户使用Bash并且存在./bash_profile时才会跳过.profile的加载,如果用户使用其他命令解释器则会自动加载.profile,所以说.profile是跨 Shell 的通用环境变量。
1,网络配置
确保所有设备(PC端,嵌入式端等等)连接在同一个局域网下,在每台设备的终端输入ifconfig查看IP地址,由于不同网络下IP地址经常改变,所有推荐将每台设备设置为静态IP地址。
使用ping
命令测试各PC之间的连通性,如果都能ping通后就可以开始下一步了
2,环境变量配置
在所有PC上编辑~/.bashrc
文件,添加以下环境变量:
bash
export ROS_MASTER_URI=http://<master_ip>:11311 # 主机的IP地址。ROS 默认使用 11311 端口 作为 roscore 的通信端口,这是由 ROS 的设计者设定的标准配置。
export ROS_IP=<local_ip> # 本机的IP地址
export ROS_HOSTNAME=<local_ip> # 本机的IP地址
然后执行
bash
source ~/.bashrc
3,主机名解析
主机名解析就是将上面的ROS_MASTER_URI=http://<master_ip>:11311中的master_ip用一个名字来代替,比如master-pc=192.168.1.1,这时只需要将192.168.1.1替换为master-pc即可,类似于变量赋值。当然,你也可以直接输入为192.168.1.1而不需要主机解析,但是这样的话如果主机的ip地址频繁变换,就需要在每个从机的./bashrc中重新修改主机的ip地址,从机数量少的话还可以,从机数量多的话就会很麻烦。
主机名解析的实现方式一般有以下几种:
1,使用/etc/hosts文件:
首先,在所有参与ROS通信的计算机上编辑/etc/hosts
文件,添加所有ROS计算机的IP和主机名映射,格式如下:
bash
192.168.1.100 master-pc
192.168.1.101 slave1-pc
192.168.1.102 slave2-pc
保存文件后测试解析:
bash
ping master-pc
ping slave1-pc
但是这种方式看起来是使用了主机解析,但它只是增强了可读性,而没有解决频繁手动修改IP的缺点,如果 Master 的 IP 地址变了,还是需要手动修改所有从机的/etc/hosts文件的配置
2,使用DNS:
DNS一般是用于学校或者企业,适用于本地有DNS服务器或路由器支持DNS绑定的场景
在路由器或 DNS 服务器上,将主机名(如 master-pc
)绑定到 Master 的 IP。所有从机自动通过 DNS 解析 master-pc
,无需手动改 IP。这样IP地址变动时,只需改DNS记录所有从机自动生效
3,使用mDNS:
适用于小型网络,如实验室或者家用
确保所有机器安装 avahi-daemon
(Linux 默认通常已安装):
bash
sudo apt install avahi-daemon # Ubuntu/Debian
直接使用 .local
主机名(无需配置 /etc/hosts
):
bash
export ROS_MASTER_URI=http://master-pc.local:11311
4,使用静态DHCP绑定:
适用于家用/实验室路由器支持DHCP静态绑定的场景
在路由器后台,将 Master 的 MAC 地址绑定到一个固定 IP(如 192.168.1.100
)。所有从机直接用这个 IP,因为 Master 的 IP 永远不会变。
4,防火墙配置
UFW(Uncomplicated Firewall)是一款基于 iptables 的简单易用的防火墙配置工具,广泛应用于基于 Debian 和 Ubuntu 的 Linux 系统中,用于管理和配置网络防火墙规则。UFW 支持允许或拒绝特定的网络连接。即使系统中没有安装 UFW,也不意味着没有防火墙阻拦网络连接。UFW 只是一个简化的防火墙管理工具,而在 Linux 系统中,底层的防火墙功能通常由 iptables 或 nftables 实现(iptables 是 Linux 系统中强大的防火墙配置工具,但它的命令复杂,规则编写难度较大,对普通用户不太友好。UFW 则提供了一种简化的接口,使用户可以用更直观、简洁的命令来管理防火墙规则,比如,启用防火墙只需执行sudo ufw enable
,而使用 iptables 则需要编写多条复杂的命令)。
先使用命令查看防火墙配置工具ufw是否安装
bash
dpkg -l | grep ufw
如果 ufw
已经安装,会显示相关的软件包信息;如果没有任何输出,说明 ufw
未安装。此时需要输入命令进行安装:
bash
sudo apt update
sudo apt install ufw
安装后查看防火墙活跃状态
bash
sudo ufw status
如果输出Status: inactive,表示防火墙处于未启用状态,网络端口处于相对开放状态,外界可以直接访问系统暴露的端口。
如果防火墙处于活跃状态,则需要确保所有PC的防火墙允许ROS通信(默认端口11311)
bash
sudo ufw allow 11311/tcp
就能允许外部设备通过 22 端口(通常用于 SSH 服务)访问本地系统
5,测试连接
在主PC上启动ROS Master:
bash
roscore
在其他PC上测试连接:
bash
rostopic list
如果连接成功,会显示 Master 上已发布的 Topic 列表。如果 Master 刚启动 roscore
,默认只有 /rosout
和 /rosout_agg
bash
$ rostopic list
/rosout
/rosout_agg
6,同步时间
需要时可以选用此功能
在ROS多机通信中,**时间同步(Time Synchronization)**是一个关键但容易被忽视的配置。指的是让所有参与ROS通信的计算机保持高度一致的系统时间(最好误差在毫秒级)。
通常使用NTP来实现时间同步 :
NTP是一种网络协议,用于同步计算机的系统时间,计算机通过NTP客户端从时间服务器(如ntp.ubuntu.com
)获取精确时间,局域网内通常可达毫秒级同步
基本原理
NTP(Network Time Protocol)实现时间同步的原理是一个分层、多源校正的精密时间同步体系,其核心设计目标是在不可靠的网络环境中实现高精度的时间同步(局域网内通常可达毫秒级,理想条件下可达亚毫秒级)。
NTP采用层级化的时间源结构,类似于金字塔:
-
Stratum 0
最顶层,直接连接原子钟、GPS或铯钟等高精度物理时钟设备,不直接参与网络通信。
- 示例:实验室原子钟、卫星时间信号。
-
Stratum 1
直接与Stratum 0设备同步的NTP服务器(时间误差通常<100μs)。
- 示例:国家授时中心服务器、Google的
time.google.com
。
- 示例:国家授时中心服务器、Google的
-
Stratum 2
从Stratum 1同步的服务器,误差逐层递增(每层增加约1ms)。
- 示例:企业级NTP服务器、公共NTP池(如
pool.ntp.org
)。
- 示例:企业级NTP服务器、公共NTP池(如
-
Stratum 3及以下
更低层级的同步节点,适用于普通客户端设备。
NTP通过以下四个时间戳计算时间偏差(Clock Offset )和网络延迟(Round-Trip Delay):
假设客户端(A)与服务器(B)交互:
-
T₁:客户端发送请求时的本地时间(A的时钟)。
-
T₂:服务器收到请求时的本地时间(B的时钟)。
-
T₃:服务器回复响应时的本地时间(B的时钟)。
-
T₄:客户端收到响应时的本地时间(A的时钟)。

最终时间偏差是多次测量的统计结果(通常采用最小二乘法 或Marzullo算法过滤异常值)。
中国的NTP服务架构是分层级的,国家授时中心是stratum1,其他运营商比如阿里云,腾讯云等部署了多台次级服务器。所以你执行命令时会使自己的PC端自动连接到附近最优的服务器,由于所有的NTP服务器本身已经同步(都溯源到国家授时中心或GPS/原子钟) ,所以连接了NTP的各个服务器就可以实现同步。
比如下面的方式一就是将其连接到了最优的服务器,但是如果你需要更高的精度,还可以指定同一NTP服务器,这时就需要在文件/etc/systemd/timesyncd.conf,添加如下内容指定特定服务器(比如阿里云)
bash
[Time]
NTP=ntp.aliyun.com
实现方式(Ubuntu系统)
方法一:使用系统默认的timesyncd(使用简单)
如果使用 systemd-timesyncd
(即 timedatectl
管理的 NTP 客户端),通常不需要额外安装 ntp
或 ntpdate
,因为 timesyncd
已经是一个轻量级的 NTP 客户端,默认集成在 systemd
中。
bash
# 检查当前同步状态
timedatectl status
# 如果未启用NTP,运行:
sudo timedatectl set-ntp on
# 手动强制同步
sudo systemctl restart systemd-timesyncd
方法二:安装完成的ntp包(更精确控制)
bash
sudo apt install ntp
sudo systemctl restart ntp # 启动或重启 NTP 服务,立即应用配置并开始同步。
# 查看同步状态
ntpq -p
ntpq -p输出示例如下
bash
remote refid st t when poll reach delay offset jitter
============================================================================
*ntp.ubuntu.com .POOL. 16 u 25 64 1 5.123 -0.432 0.871
+time.google.com .GOOG. 1 u 12 64 7 1.234 0.567 0.123
-
*
表示当前主同步源。 -
offset
:本地时钟与服务器的偏差(单位:毫秒)。 -
jitter
:网络延迟的波动程度。
验证时间同步:
bash
ntpdate -q 192.168.1.100 # 查询与某台机器的时间差
7,共享ROS包
此功能如果需要可以选用,不做详细解释
方法1:同步工作空间
-
使用rsync或git同步工作空间
-
确保所有PC上的包路径相同
方法2:NFS共享
-
设置NFS服务器共享工作空间
-
其他PC挂载该共享目录
方法3:独立编译
-
在各PC上独立维护代码库
-
确保版本一致
8,启动管理
可以使用roslaunch
在多台机器上启动节点:
XML
<launch>
<machine name="pc1" address="192.168.1.101" user="user" password="pass" env-loader="/opt/ros/noetic/env.sh"/>
<machine name="pc2" address="192.168.1.102" user="user" password="pass" env-loader="/opt/ros/noetic/env.sh"/>
<node machine="pc1" name="node1" pkg="your_pkg" type="node1"/>
<node machine="pc2" name="node2" pkg="your_pkg" type="node2"/>
</launch>