1. 先把"连接"这个词拆开:三种形态,选错了后面全是白费劲
很多人张口就是"VSCode 连 Ubuntu",但这句话在实践里至少对应三种完全不同的形态,选错分支之后,后面所有配置都会变成在错误的路上使劲。我自己第一次折腾的时候,就因为在虚拟机上乱挂共享目录,导致 CMake 每次全量重扫要等两分钟,换成 Remote-SSH 之后同一份工程增量构建从 90 秒掉到 6 秒,那一刻才真正明白"选对连接方式"比"调参优化"重要得多。
第一种是WSL:Ubuntu 以子系统形式跑在 Windows 上,和宿主共用内核与内存。装完之后在 Ubuntu 终端里敲code .,VSCode 会自动以"已连接 WSL"的形式打开,本质上是 VSCode 在 Windows 上跑界面、在 WSL 里跑服务端。它最大的好处是零网络配置、文件系统直通、可以用 Windows 的显卡和输入法。代价是它不是一台真正的机器,内核模块、systemd 的部分能力、底层网络行为都和独立系统有差异。
第二种是Remote-SSH:Ubuntu 是一台独立的系统——可以是局域网里的物理机、可以是 VMware/VirtualBox 里的虚拟机、也可以是云上的实例。VSCode 通过 SSH 通道把一个小服务端推过去,之后编辑、搜索、终端、调试全部在 Ubuntu 那一侧执行,Windows 只负责画界面。这种方式最贴近"真实开发机"的体验,也是团队协作里最通用的做法。
第三种是共享目录式:虚拟机上配个共享文件夹,Windows 侧用 VSCode 打开那个目录。严格说这不算"连接",只是把文件映射过来了。它的致命问题在于跨文件系统的 IO 放大——Windows 打开一次文件,虚拟机侧可能产生几十次元数据查询,node_modules或者大型 C++ 工程在这种模式下编辑器索引会慢到让人怀疑人生。
1.1 三种形态到底差在哪
| 维度 | WSL | Remote-SSH | 共享目录 |
|---|---|---|---|
| 交互延迟 | 极低 | 取决于网络,局域网内几乎无感 | 低 |
| 大项目文件IO | 快(放在 Linux 文件系统内) | 快 | 很慢 |
| 是否需要网络配置 | 不需要 | 需要,IP/端口/防火墙都要通 | 不需要 |
| 环境独立性 | 与宿主共用内核 | 完全独立 | 完全独立 |
| 能否跑底层内核相关工具 | 部分受限 | 完全支持 | 完全支持 |
| 适合场景 | 日常脚本、Python、Web 前端 | 编译型语言、嵌入式、需要真机环境 | 临时看一眼文件 |
这张表不是让你背,而是让你在动手前先问自己一个问题:我要跑的东西,依赖内核模块吗?依赖真实硬件吗?如果答案是否定的,WSL 是成本最低的选择;如果答案是肯定的,直接上 Remote-SSH,不要试图用共享目录绕过去。
1.2 一个容易踩的认知坑:双系统连不了
"我装了双系统"和"我用 VSCode 连 Ubuntu"这两件事存在天然冲突。双系统的意思是同一时刻只有一套系统在运行,Ubuntu 没启动的时候,SSH 服务根本不存在,自然连不上。所以如果你的目标是让 Windows 上的 VSCode 随时连过去写代码,正确做法是虚拟机常驻或者另有一台常开的机器,而不是双系统。
提示:虚拟机场景下,网络模式选"桥接"最省心,虚拟机会像一台独立设备一样拿到局域网 IP;选 NAT 的话必须在虚拟机软件里做端口转发,多一层配置就多一个出错点。
2. Ubuntu 这一端要先把门打开:从网络拓扑到 sshd 可用
VSCode 连不上,九成的问题不在 VSCode,而在 Ubuntu 侧的门没开或者开错了位置。我在帮同事排查这类问题时,几乎从来不先看 VSCode 的报错窗口,而是先在 Ubuntu 上执行三条命令,把问题范围一刀切开。
2.1 先在 Ubuntu 上确认"我在哪、我叫什么"
打开 Ubuntu 的终端,依次执行:
ip -4 addr show | grep inet hostname -I第一条会列出所有网卡的 IPv4 地址。你要关注的是那个看起来像192.168.x.x或10.x.x.x的地址,127.0.0.1是你自己,别人连不进来。第二条只打印地址,输出更干净,适合直接复制。
如果是虚拟机,还要额外确认一件事:虚拟机的网络模式和你拿到的 IP 段是否自洽。桥接模式下虚拟机拿到的地址应该和宿主机在同一个网段;NAT 模式下虚拟机通常拿到192.168.56.x这类内部段,宿主机访问它需要端口转发规则,而不是直接连 IP。
2.2 安装并检查 SSH 服务
Ubuntu 桌面版默认不装SSH 服务端,这一点非常容易忽略——很多人以为是防火墙问题,其实根本没有服务在监听。
sudo apt update sudo apt install -y openssh-server systemctl status ssh看到active (running)才算起步。如果状态是failed,先看journalctl -u ssh -n 50的尾部输出,常见原因是 22 端口被别的进程占用,或者配置文件里写了非法指令。
确认监听端口:
ss -tlnp | grep sshd输出里应该能看到0.0.0.0:22或者*:22。如果只看到127.0.0.1:22,说明配置里绑定了回环地址,外部连接一定会被拒,需要去/etc/ssh/sshd_config检查ListenAddress这一项。
防火墙方面,Ubuntu 默认的 ufw 一般是关闭状态,如果你手动开过,记得放行:
sudo ufw status sudo ufw allow 22/tcp2.3 把密码登录换成密钥登录
密码登录能用,但每次连接都要输、脚本里不好自动化、而且密码强度一旦不够就是隐患。密钥登录配一次,后面几年都省事。
在 Windows 侧(用 PowerShell 或 Git Bash)执行:
ssh-keygen -t ed25519 -C "dev@workstation"一路回车,默认生成在C:\Users\你的用户名\.ssh\下,得到id_ed25519(私钥)和id_ed25519.pub(公钥)。私钥不要发给任何人,也不要复制到 Ubuntu 上,这是最容易搞混的一点。
把公钥送过去,Ubuntu 侧只需要一条命令:
mkdir -p ~/.ssh && chmod 700 ~/.ssh echo "把公钥内容粘到这里" >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keyschmod这两条不是可选项,SSH 对权限极其敏感,~/.ssh是 755 或者authorized_keys是 644 的时候,服务端会直接忽略这个文件,表现就是"公钥明明放进去了却还要密码"。这个坑我见过太多次,检查权限应该是排查密钥失效的第一步。
确认密钥可用之后,可以关掉密码登录:
# /etc/ssh/sshd_config PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin no改完执行sudo systemctl restart ssh。注意:改配置之前务必先开另一个终端验证密钥能登录,否则一旦配置写错,你可能连回去改的机会都没有。
2.4 开发环境还需要提前做的三件小事
第一件是提高文件监听上限。VSCode 的文件监听、热重载、很多构建工具都依赖 inotify,默认值在稍大的工程里会直接爆掉:
cat /proc/sys/fs/inotify/max_user_watches # 偏小的话 echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf sudo sysctl -p第二件是给用户加到需要的组里。比如要用 Docker 却不加组,每次都得 sudo,而 VSCode 的远程终端里 sudo 交互很别扭:
sudo usermod -aG docker $USER加完组必须重新登录(关掉所有 SSH 会话重连),否则组信息不会刷新,很多人改完发现"没生效"就是这一步漏了。
第三件是看一眼磁盘空间。df -h如果根分区快满了,VSCode 的服务端解压会失败,报错却往往显示成"连接超时",方向完全跑偏。留出至少 2~3 GB 比较保险。
3. VSCode 这一端:插件装哪些、config 怎么写、第一次握手发生了什么
Ubuntu 那边门开了,接下来才是 VSCode 的活儿。这一章的重点不是"点哪个按钮",而是理解 VSCode 远程架构的分层,理解之后很多诡异现象自己就能解释。
3.1 插件清单:装三个就够,多装反而是负担
必装的是Remote - SSH(连独立机器)、WSL(连子系统)。微软有个Remote Development扩展包,把 SSH、WSL、容器三种能力打包在一起,图省事可以直接装它,代价是拖进来几个你可能永远用不到的东西。
其他插件不要在本地一股脑装完再去连远程。原因在 3.4 会讲清楚。
有一个例外:中文语言包。它属于 UI 层插件,装在本地就能让界面变中文。但如果你想让远程的终端提示、任务输出也是中文,需要在远程侧再装一次对应的语言包,两边是独立的。
3.2 把 ~/.ssh/config 写对,后面的路会顺很多
大部分人第一次连是直接敲ssh user@192.168.1.100,然后每次都要回忆 IP、用户名、端口。正确做法是在 Windows 侧C:\Users\你的用户名\.ssh\config里定义别名:
Host dev-ubuntu HostName 192.168.1.100 User yourname Port 22 IdentityFile ~/.ssh/id_ed25519 ServerAliveInterval 30 ServerAliveCountMax 6 ControlMaster auto ControlPath ~/.ssh/cm-%r@%h:%p ControlPersist 10m几个关键项值得单独说。ServerAliveInterval 30表示每 30 秒发一次心跳,配合ServerAliveCountMax 6,可以在网络抖动或者路由表刷新时让会话多撑几分钟而不是立刻断。ControlMaster这一段是连接复用:VSCode 连远程时会开多条 SSH 通道(终端、文件同步、端口转发各自独立),不复用的话你可能看到十几个 ssh 进程,复用了之后底层只有一条 TCP,重连和断线恢复都明显更快。
写完 config 之后,验证一步:
ssh dev-ubuntu能免密进去,说明网络、密钥、配置三层都通了。这一步没通之前不要打开 VSCode,否则你会在 VSCode 的一堆日志里找问题,效率极低。
3.3 第一次连接时,VSCode 在后台干了什么
点"连接到主机"之后,会依次发生这些事:建立 SSH 通道 → 检测远程架构(x86_64/aarch64)→ 检查~/.vscode-server是否存在 → 不存在则把服务端压缩包传过去解压 → 启动远程扩展宿主进程 → 加载工作区。
理解这个流程的价值在于:卡在不同的阶段,问题是完全不同的。卡在"正在下载 VS Code 服务器"通常是网络或磁盘问题;卡在"正在打开远程连接"多半是服务端进程起不来;连上之后窗口一片空白,往往是远程扩展宿主崩溃或者用户主目录配额满了。
服务端落在~/.vscode-server/下面,里面按 commit id 分目录。这个目录会随着 VSCode 升级不断累积旧版本,用一段时间后可以清理掉不在用的那些,能腾出不少空间。
3.4 本地插件和远程插件是两套,这是最容易踩的坑
VSCode 在远程模式下把扩展分成两类:UI 扩展跑在本地,工作区扩展跑在远程。你装了一个 C/C++ 插件,它需要在远程侧运行才能解析远程的include路径;你装了一个主题插件,它跑在本地就够了。
于是就有了那个经典现象:明明装了插件,为什么功能不生效。答案通常是插件装在了本地,而当前窗口是远程窗口,VSCode 会把它标记为"需要在远程安装"。正确做法是打开扩展面板,切到对应分类看一眼,需要远程的就在远程装。
这里的实用建议是:能装远程就装远程。原因是插件在本地运行、要频繁访问远程文件时,每一次操作都要走 SSH 通道,延迟叠加起来非常难受。远程安装则是在服务端本地读文件,快得不是一个量级。
4. 连上只是开始:编译、调试、输入法三件套怎么配
窗口右下角显示"SSH: dev-ubuntu"的那一刻,很多人以为大功告成,其实真正的配置工作才刚开始。远程窗口里跑的是 Ubuntu 的环境,而 Ubuntu 上有什么工具链,取决于你刚才装了多少东西。
4.1 工具链一次性补齐,别边写边装
基础组合我一般这么装:
sudo apt install -y build-essential gdb cmake ninja-build pkg-config git curlbuild-essential会把gcc、g++、make、libc头文件一起带上。单独装gcc有时会遇到"安装失败"或者装完编译还报找不到头文件,就是因为缺了libc6-dev这一类依赖,直接装build-essential省事得多。
如果apt install报错,按这个顺序看:先看是不是有另一个 apt 进程在跑(ps aux | grep apt),再sudo dpkg --configure -a修一下中断的安装,最后看df -h磁盘是不是满了。这三个原因能覆盖绝大多数安装失败。
CMake 版本是另一个高频问题。Ubuntu 仓库里的 CMake 往往偏旧,而新一点的项目会要求 3.20 以上。检查版本:
cmake --version升级有两条路:一是用 pip 装(适合个人开发环境,pip install --user cmake),二是加 Kitware 的官方源。pip 那条路更干净,出问题直接删掉包目录就行,不会污染系统。
4.2 C/C++ 的远程调试:三个文件决定成败
远程 C++ 调试需要.vscode/下的三个文件配合:c_cpp_properties.json管代码跳转和补全,tasks.json管怎么编译,launch.json管怎么启动调试器。很多人的问题出在这三个文件里写的路径是 Windows 风格的——C:\Users\...在远程 Linux 上根本不存在。
c_cpp_properties.json关键在compilerPath要指向远程的编译器:
{ "configurations": [ { "name": "Linux", "compilerPath": "/usr/bin/g++", "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "linux-gcc-x64", "compileCommands": "${workspaceFolder}/build/compile_commands.json" } ] }最省事的办法其实是最后那行compileCommands:让 CMake 生成compile_commands.json,插件直接读它,头文件路径、宏定义、编译选项全都自动同步,不用手写一堆includePath。
CMake 生成这个文件只要在CMakeLists.txt里加一句:
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)调试用的launch.json里,program路径必须是 Linux 路径,通常是${workspaceFolder}/build/你的可执行文件,MIMode设为gdb,miDebuggerPath写/usr/bin/gdb。如果启动时报"找不到 gdb",说明远程没装,回去执行 4.1 那条 apt 命令。
注意:调试器的路径一定要用
which gdb的输出确认,别凭记忆写。Ubuntu 里 gdb 装在/usr/bin/gdb是常态,但用 snap 或者自己编译过的情况就未必。
4.3 Python 解释器选错,是最隐蔽的坑
Python 项目在远程模式下最容易出现的问题是:终端里pip list有某个包,但 VSCode 里导入还是标红。原因通常是解释器不一致——VSCode 用了系统 python,终端里用的却是 conda 或者是虚拟环境。
处理办法是显式指定。打开命令面板选Python: Select Interpreter,然后手动填路径,不要只在列表里挑名字。虚拟环境的解释器路径长这样:
/home/yourname/projects/demo/.venv/bin/python判断当前 VSCode 用的是哪个解释器,看一眼状态栏左下角就行。装依赖的时候也用这个解释器的绝对路径去装:
/home/yourname/projects/demo/.venv/bin/pip install -r requirements.txt这样能保证装的位置和用的位置永远是同一个,避免"我明明装了"的扯皮。
4.4 中文输入和界面语言
远程窗口里能不能打中文,取决于两个层面:输入法框架装在 Ubuntu 上,输入法候选框由本地绘制。
Ubuntu 侧装 fcitx5 加拼音输入法:
sudo apt install -y fcitx5 fcitx5-chinese-addons装完需要配置环境变量让 GTK 和 Qt 程序知道用哪个输入法框架。这几个变量写在~/.profile或者~/.pam_environment里:
GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx改完注销重新登录生效。这里有个细节:环境变量写在~/.bashrc里对图形程序无效。~/.bashrc只在交互式 shell 里被读,图形会话启动的程序根本不会加载它。这就是"终端里echo $GTK_IM_MODULE有值,但输入法还是不好用"的根本原因。
界面语言方面,装中文语言包之后如果没生效,检查一个设置项:Configure Display Language是否选了zh-cn。这个设置是按窗口类型分开的,本地窗口和远程窗口各自记一份,所以可能出现"本地是中文、远程是英文"的情况,两个窗口各设一次就好。
4.5 环境变量在 VSCode 里不生效,问题出在哪
这是远程开发里最让人抓狂的一类问题:终端里echo $PATH明明包含了~/bin,可是 VSCode 的任务、调试器却找不到那个可执行文件。
根因在于VSCode 的远程服务端不是由交互式 shell 启动的。它通过 SSH 的非交互通道启动,只加载~/.profile(或者 shell 的非交互配置),不读~/.bashrc。所以你在.bashrc里加的那些export,对终端面板有效(终端会启动交互式 shell),对任务和调试进程无效。
最干净的解法是把路径类的配置放进~/.profile,然后用一条兼容写法兜底:
# ~/.profile export PATH="$HOME/.local/bin:$HOME/bin:$PATH"如果你不想动系统文件,也可以在项目的.vscode/settings.json里给终端显式注入:
{ "terminal.integrated.env.linux": { "PATH": "/home/yourname/.local/bin:${env:PATH}" } }两种方式我都在用,前者适合全局工具,后者适合只对某个项目生效的临时配置。
5. 连不上的完整排查链路:从超时到服务端起不来
这一章我按真实排查顺序写,你可以照着一步步走。原则是分层验证:先证明网络通,再证明 SSH 通,最后才怀疑 VSCode。
5.1 第一层:网络到底通不通
从 Windows 侧执行:
ping 192.168.1.100如果 ping 不通,先别碰 SSH。检查虚拟机网络模式、IP 是否变了(DHCP 租约到期换 IP 是很常见的)、Windows 防火墙是否把出站拦了。虚拟机上如果用的是 NAT,ping 本来就不一定通,这时直接跳到下一层用端口测试。
# Windows PowerShell Test-NetConnection 192.168.1.100 -Port 22TcpTestSucceeded : True说明端口可达。这一步比 ping 更有说服力,因为 ICMP 经常被策略拦掉,但 TCP 22 通不通才是真正决定能不能连的。
5.2 第二层:SSH 本身能不能过
ssh -vvv dev-ubuntu-vvv会打印完整的握手过程。看三个关键位置:Connecting to ...后面有没有往下走,说明 TCP 建起来了;Authentications that can continue列出的是什么,能看出服务端允许哪些认证方式;Offering public key之后有没有Server accepts key,能看出密钥是否被接受。
如果卡在Offering public key然后失败,八成是authorized_keys权限不对,回到 2.3 节检查。
5.3 第三层:VSCode 特有的问题
到这里 SSH 已经能通了,但 VSCode 还是连不上,那就只剩服务端的事。打开命令面板执行Remote-SSH: Show Log,看远程连接日志的尾部。
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 卡在"正在下载 VS Code 服务器" | 目标机访问不了下载源,或磁盘不足 | 确认磁盘空间,必要时手动放置服务端包 |
| 报"Failed to parse remote port" | 远程 shell 输出了额外内容干扰解析 | 检查~/.bashrc里有没有echo打印 |
| 连上后扩展全部需要重装 | 服务端 commit 目录变了 | 属正常现象,重新安装即可 |
| 终端能开但窗口空白 | 远程扩展宿主崩溃 | 删除对应版本的~/.vscode-server目录重连 |
| 频繁掉线 | 网络空闲被回收 | 加ServerAliveInterval |
第三行那个"shell 输出干扰"值得展开说一句:如果你在~/.bashrc里写了echo "欢迎登录"或者跑了个会打印 banner 的脚本,VSCode 解析远程主机信息时会把这些输出当成数据,直接解析失败。所有在非交互场景下会打印内容的配置都该用case $- in *i*)包起来,只在交互式 shell 里执行。
5.4 内网和离线环境怎么处理
有些开发机在隔离网络里,出不去外网,VSCode 服务端下载会一直转圈。思路是在能上网的机器上拿到服务端包,再传进目标机。
VSCode 的远程服务端包可以从官方发布的地址按 commit id 下载,你的 VSCode 版本对应的 commit id 可以在"帮助 → 关于"里看到。拿到包之后,在目标机上解压到~/.vscode-server/bin/<commit-id>/下面,确保目录结构里直接是bin、node、out这些内容,而不是多套了一层目录。放好之后重连,VSCode 会在本地校验,通常就直接跳过了下载步骤。
另一种更省事的做法是在离线环境里预先准备好一台"模板机",把~/.vscode-server和工具链都装好,之后克隆虚拟机或者拷用户目录即可,比每次重新走一遍下载流程靠谱得多。
6. 用久了才明白的几个细节,能省下大量来回折腾的时间
前面五章是"从零到连上",这一章是"连上之后怎么用得舒服"。这些点很少有人一开始就讲,但每一个都能在长期使用里省下不少时间。
6.1 端口转发:本地浏览器直接看远程服务
远程跑了个 Web 服务监听 8080,你没必在 Ubuntu 上开浏览器。VSCode 的端口面板可以自动发现监听端口,也可以手动添加转发。添加之后,本地浏览器访问localhost:8080就直达远程服务。
命令行侧不想开面板的话,直接在 config 里预置:
Host dev-ubuntu ... LocalForward 8080 127.0.0.1:8080这条规则在连接建立时就生效,适合那些每次都要用的固定服务。
6.2 工作区文件与多窗口
频繁切换的项目,建议用.code-workspace工作区文件把多个目录打包在一起打开,比如同时打开后端仓库、前端仓库和一份文档目录。远程模式下工作区文件也保存在远程,换台电脑连过去,配置跟着走。
多窗口方面要注意一点:同一个远程主机可以开多个窗口,但每个窗口会各起一套扩展宿主进程,内存占用是按窗口叠加的。机器内存不大的话,同时开三四个远程窗口再加编译,很容易把内存吃满,表现出来就是"编辑器莫名其妙变卡"。
6.3 长任务的断线处理
编译、跑测试这类长任务最怕断线。两个实用做法:一是把重要任务放到tmux或者nohup里跑,VSCode 断不断都不影响;二是把 SSH 的心跳参数配好(3.2 节里的ServerAliveInterval),大部分短暂抖动都能扛过去。
任务跑完再重连的情况下,用tmux attach回到会话里看完整输出,比在 VSCode 终端里翻历史记录方便得多。我现在的习惯是:超过五分钟的任务一律进 tmux,VSCode 终端只用来做交互式操作。
最后分享一个我自己踩出来的经验:远程开发环境的配置文件(.ssh/config、~/.vscode-server的清理脚本、工具链的安装脚本)都值得单独存一份,用 Git 管理起来。换机器或者重装系统的时候,把这些脚本跑一遍,二十分钟就能恢复到熟悉的状态;靠记忆一条条敲,往往要折腾一整天,还容易漏掉某个当时觉得"以后再说"的细节。