1. 为什么一个启动脚本值得单独拿出来聊
玩过 KernelSU 的人大概都有这个体会:root 权限拿到了,模块也刷进去了,日常用起来挺顺,但一旦涉及到"每次 su 会话都要带某个环境变量"或者"进 root shell 之前先跑几条初始化命令"这种需求,就开始抓瞎。大多数人第一反应是去改/system/etc下的某个脚本,或者干脆写个 Magisk 风格的post-fs-data.sh塞进模块里,结果发现要么时机不对,要么作用域不对,要么重启之后被覆盖。
.ksurc这个文件就是专门解决这类问题的。它是 KernelSU 提供的一个用户级启动配置文件,作用类似于你登录 Linux 服务器时 bash 自动读取的.bashrc——每次通过su进入 root 会话时,KernelSU 会先把这个文件里的内容执行一遍,然后再把控制权交给你。听起来简单,但它背后牵扯到的东西不少:执行时机、环境变量继承、shell 类型判断、权限边界,以及和模块脚本之间的优先级关系。
我最初注意到这个功能,是因为一个很具体的场景:某台测试机上跑着一个需要LD_LIBRARY_PATH指向自定义库目录的调试工具,每次adb shell su进去都得手动 export 一遍,烦得不行。后来翻 KernelSU 的文档才想起来有.ksurc这回事,试了一下,五分钟搞定,从此再没手动敲过。这件事让我意识到,很多 KernelSU 用户其实并不知道这个文件的存在,或者知道但没搞清楚它到底能干什么、不能干什么。
这篇内容就是把这个文件掰开揉碎讲清楚:它放在哪、什么时候被执行、能写什么、不能写什么、和模块脚本怎么配合、踩过哪些坑。适合已经刷了 KernelSU、对 root 环境有一定了解、想让自己的 su 会话更"顺手"的读者。完全没接触过 KernelSU 的朋友也能看,但最好先补一下基本的 root 概念,不然有些地方会有点跳。
2. .ksurc 到底是什么:定位与执行机制拆解
2.1 它不是一个"模块脚本",别搞混了
很多人第一次听说.ksurc,会下意识把它和 Magisk 模块里的post-fs-data.sh、service.sh归为一类。这是个典型的误解,而且这个误解会直接导致你把脚本放错位置、写错内容。
模块脚本是系统级的,开机过程中由守护进程在特定阶段执行,跑在系统启动的早期或中期,作用对象是整个系统环境。而.ksurc是会话级的,它只在有人通过su请求 root 权限、并且请求被批准之后,才在即将创建的那个 root shell 里被执行。换句话说,模块脚本是"开机就跑一次",.ksurc是"每次进 root 就跑一次"。
这个区别带来的直接影响是:你不能指望在.ksurc里做那些需要在系统启动早期完成的事情,比如挂载分区、修改系统属性、启动守护进程。反过来,你也不应该在模块脚本里写那些只对交互式 shell 有意义的别名和环境变量——写了也没用,因为模块脚本执行时的环境和你的交互式会话不是一回事。
我见过有人把一堆alias写进service.sh,然后抱怨"为什么进 su 之后别名不生效"。这就是典型的定位混淆。别名、提示符、PATH追加、临时环境变量,这些才是.ksurc的主场。
2.2 执行时机:在 shell 初始化之后,在命令执行之前
要理解.ksurc的执行时机,得先理清su的整个流程。当你敲下su或者某个应用请求 root 时,KernelSU 的守护进程会做权限校验,校验通过后 fork 出一个新进程,切换 uid/gid 到 root,然后启动一个 shell。.ksurc就是在 shell 启动之后、但还没开始读取你的输入之前被 source 进去的。
这里有个关键细节:它是被source而不是被exec的。source 意味着脚本在当前 shell 进程里执行,脚本里设置的变量、定义的函数、改的PATH,都会直接影响你接下来拿到的这个 shell。如果是 exec,那就是另起一个进程,改什么都传不回来。这个设计选择很合理——.ksurc的目的就是定制当前会话,source 是唯一正确的做法。
但也正因为是 source,脚本里任何一条命令出错、任何一个exit,都可能把整个 shell 搞挂。你在.ksurc里写个exit 1,那 su 会话直接就退了,连提示符都看不到。这一点后面讲避坑的时候还会展开。
2.3 文件位置与查找顺序
.ksurc的标准位置在 root 用户的家目录下,也就是/data/adb/ksu/.ksurc。这个路径是 KernelSU 约定的,不需要你额外配置。有些版本或分支可能会在/data/adb/.ksurc也做一次查找,但以官方主线为准,认准/data/adb/ksu/.ksurc这个路径最稳妥。
为什么放在/data/adb/ksu/下面而不是/root/?因为 Android 上根本没有传统意义上的/root家目录,root 用户的 HOME 环境变量在不同 shell、不同上下文里指向可能不一致。KernelSU 把配置集中在自己管理的目录下,既避免了路径歧义,也方便统一管理权限。/data/adb/ksu/这个目录本身权限就是 700 且属主是 root,普通应用碰不到,安全性有保障。
顺带提一句,如果你用的是某个定制版或者较老的版本,发现标准路径下放了文件但不生效,可以去查一下该版本的文档,确认它实际查找的路径。这种版本差异在开源项目里很常见,别想当然。
2.4 和 shell 类型的关系
.ksurc的内容最终是被当前使用的 shell 解释执行的。KernelSU 默认给 root 会话配的 shell 通常是系统自带的sh(在 Android 上一般是mksh或bash的某个精简版),具体取决于你的系统镜像和 KernelSU 的配置。
这意味着你写在.ksurc里的语法必须是目标 shell 支持的。如果你习惯了 bash 的[[ ]]、数组、function关键字,而实际执行的是mksh,那部分语法可能不认,轻则报错重则中断。稳妥的做法是:要么确认你的环境确实是 bash,要么就用 POSIX 兼容的写法,[ ]、普通函数定义、避免数组。这个取舍后面在实操部分会具体演示。
3. 核心能力边界:能做什么,不能做什么
3.1 环境变量定制:最主力也最安全的用法
.ksurc最典型、最没有争议的用法就是设置环境变量。PATH追加自定义 bin 目录、LD_LIBRARY_PATH指向私有库、各种工具需要的配置路径、语言和时区相关的LANG/TZ,这些都属于"只影响当前会话、不影响系统全局"的范畴,放在.ksurc里恰到好处。
举个我自己的例子。我在测试机上放了一套静态编译的调试工具在/data/local/tmp/tools/bin,希望每次进 root 都能直接调用,不用敲全路径。.ksurc里就一行:
export PATH="/data/local/tmp/tools/bin:$PATH"注意这里是$PATH在前还是自定义路径在前,顺序有讲究。把自定义路径放前面,意味着如果系统里有个同名命令,优先用你的;放后面则相反。我一般倾向放前面,因为放这套工具的目的就是要用它,但如果你只是补充一些系统没有的命令,放后面更安全,避免意外覆盖系统命令导致行为异常。
LD_LIBRARY_PATH的坑更多一些。Android 的链接器对库路径有额外限制,某些情况下即使设了LD_LIBRARY_PATH也不生效,尤其是涉及到系统分区的可执行文件时。这个不是.ksurc的问题,是 Android 链接器本身的策略。遇到这种情况,要么用patchelf改 rpath,要么把库放到链接器默认搜索的目录里,光靠环境变量解决不了。
3.2 别名与函数:提升交互效率
交互式会话里,别名和函数能省不少事。比如我经常需要看某个目录下的日志,就会在.ksurc里定义:
alias ll='ls -al' alias logs='cd /data/local/tmp/logs && ls -lt'函数则适合稍微复杂一点的逻辑,比如一个带默认参数的清理命令:
clean_tmp() { target="${1:-/data/local/tmp/cache}" [ -d "$target" ] && rm -rf "$target"/* && echo "cleaned: $target" }这里用了${1:-默认值}的写法,POSIX 兼容,mksh 和 bash 都认。函数定义本身不执行任何东西,只是注册到当前 shell,所以放在.ksurc里没有副作用,很安全。
但要注意,别名和函数只在交互式shell 里有意义。如果你的 su 会话是被脚本调用的(比如su -c "some command"),那.ksurc可能根本不会被执行,或者执行了但别名对非交互式命令无效。这个差异后面讲排查的时候会重点说。
3.3 提示符定制:让 root 会话一眼可辨
默认的 root 提示符往往和普通 shell 长得差不多,一不小心就在 root 下执行了本该普通用户跑的命令。在.ksurc里改一下PS1,让 root 会话有醒目的标识,是个成本极低、收益很高的习惯:
export PS1='\[\e[31m\][root]\[\e[0m\] \w # '这段在 bash 下能把提示符变成红色的[root]加当前路径。但如果你用的是 mksh,\[\e[31m\]这种转义序列的解析可能不一样,颜色出不来甚至显示乱码。稳妥的写法是先判断 shell 类型,或者干脆用最朴素的纯文本提示符PS1='[root] \w # ',牺牲颜色换兼容性。我个人的做法是:不确定环境时就用纯文本,确认是 bash 再上颜色。
3.4 明确不能做的事:别把系统级操作塞进来
这一节是重点,因为踩坑的人最多。.ksurc里不应该出现以下内容:
- 挂载操作(
mount、mount -o remount):这类操作应该在模块的post-fs-data.sh里做,.ksurc执行时系统早就启动完了,此时挂载要么失败要么产生意料之外的副作用。 - 启动长期运行的守护进程:
.ksurc是每次 su 都执行的,你在里面nohup some_daemon &,那每进一次 root 就多一个进程,很快就一堆重复进程。守护进程该放service.sh。 - 修改系统属性(
setprop改关键属性):会话级脚本改全局属性,影响范围失控,而且重启即失效,没有意义。 - 依赖网络的操作:
.ksurc执行时网络状态不确定,而且每次 su 都联网检查会拖慢会话启动,体验极差。 - 任何
exit语句:前面说过,source 执行,exit会直接终结 shell。
把这些边界记牢,能避免八成的"为什么我的 su 进去就闪退"问题。
4. 实操:从零写一份可用的 .ksurc
4.1 准备工作:确认路径与权限
动手之前先确认两件事:目标目录存不存在、当前权限对不对。用任意能拿到 root 的方式(比如已经配好的 su)执行:
ls -ld /data/adb/ksu正常应该看到属主 root、权限drwx------或类似。如果目录不存在,说明你的 KernelSU 版本可能较老或者安装不完整,先解决这个再谈.ksurc。目录存在的话,直接创建文件:
touch /data/adb/ksu/.ksurc chmod 600 /data/adb/ksu/.ksurc chown root:root /data/adb/ksu/.ksurc权限设 600 是必须的。这个文件里可能包含路径信息甚至敏感配置,不能让其他用户读到。600 意味着只有 root 能读写,其他一律无权。别图省事设 644,那等于把配置暴露给所有应用。
4.2 第一版:最小可用脚本
先写一个最保守的版本,只做环境变量和别名,确保能跑通再逐步加东西:
# /data/adb/ksu/.ksurc # 最小可用版本:只做环境变量与别名 export PATH="/data/local/tmp/tools/bin:$PATH" export EDITOR=vi alias ll='ls -al' alias ksu-ver='cat /data/adb/ksu/version 2>/dev/null || echo unknown'写完之后不要急着退出当前会话测试,因为.ksurc只在新的 su 会话里生效。正确做法是:保存文件,退出当前 root shell,重新su进去,然后验证:
echo $PATH alias ll如果PATH里出现了你的自定义目录、alias ll能列出定义,说明基本链路通了。这一步看着简单,但它是后面所有复杂配置的地基,务必先确认。
4.3 加入条件判断:让脚本更健壮
最小版本跑通之后,就可以加一些防御性逻辑了。真实环境里,路径可能不存在、命令可能缺失,脚本不能假设一切完美。改造一下:
# 仅在目录存在时追加 PATH [ -d /data/local/tmp/tools/bin ] && \ export PATH="/data/local/tmp/tools/bin:$PATH" # 根据 shell 类型设置提示符 case "$0" in *bash*) export PS1='\[\e[31m\][root]\[\e[0m\] \w # ' ;; *) export PS1='[root] \w # ' ;; esac[ -d ... ] && export ...这个写法很实用:目录不存在就跳过,不会报错也不会污染PATH。case "$0"判断当前 shell 的可执行文件名,虽然不百分百准确(有些环境$0是su而非 shell 名),但作为粗略判断够用了。更严谨的做法是查$BASH_VERSION是否存在,存在就是 bash:
if [ -n "$BASH_VERSION" ]; then export PS1='\[\e[31m\][root]\[\e[0m\] \w # ' else export PS1='[root] \w # ' fi这个判断比$0可靠,推荐用这个。
4.4 参数化与可维护性:别写成一次性脚本
.ksurc一旦开始变长,就得考虑可维护性。我的习惯是把可变的部分抽成变量放在文件开头,后面统一引用:
TOOLS_DIR="/data/local/tmp/tools/bin" LOG_DIR="/data/local/tmp/logs" [ -d "$TOOLS_DIR" ] && export PATH="$TOOLS_DIR:$PATH" alias logs="cd $LOG_DIR && ls -lt"这样以后换路径只改一处,不用满文件找。另外,给每个逻辑块加一行注释说明意图,三个月后回来看还能秒懂。这不是矫情,是实打实省时间——我见过太多人(包括我自己)回头改自己写的启动脚本时,对着几行没注释的 export 发呆。
4.5 验证与回滚:留好退路
改.ksurc有个风险:写错了可能导致新开的 su 会话行为异常,严重时甚至进不去。所以每次大改之前,先备份:
cp /data/adb/ksu/.ksurc /data/adb/ksu/.ksurc.bak万一改完发现 su 进去就报错或者卡住,用su -c这种非交互方式执行命令来恢复(前提是非交互方式不加载.ksurc,多数情况下如此):
su -c "cp /data/adb/ksu/.ksurc.bak /data/adb/ksu/.ksurc"如果连su -c都受影响,那就得靠 recovery 或者 adb root 之类的兜底手段了。所以备份这一步千万别省,尤其是你打算往.ksurc里加复杂逻辑的时候。
5. 进阶玩法与组合场景
5.1 按项目切换环境:多套配置的思路
如果你同时维护几个不同的调试项目,每个项目需要不同的环境变量,可以在.ksurc里做一层简单的"环境切换"逻辑。比如约定一个标记文件,存在就加载对应配置:
PROFILE_FILE="/data/local/tmp/.current_profile" if [ -f "$PROFILE_FILE" ]; then profile=$(cat "$PROFILE_FILE") case "$profile" in projA) export PROJ_HOME=/data/local/tmp/projA ;; projB) export PROJ_HOME=/data/local/tmp/projB ;; esac fi这样切换项目时只要改标记文件内容,不用动.ksurc本身。这个模式在需要频繁切换上下文的场景里很省心,本质上是把"配置选择"和"配置内容"解耦了。
5.2 与模块脚本的分工协作
一个完整的 root 环境定制,通常是模块脚本和.ksurc配合完成的。分工原则很清晰:
| 需求类型 | 放哪里 | 原因 |
|---|---|---|
| 挂载、改属性、启动守护进程 | 模块post-fs-data.sh/service.sh | 需要系统级、开机早期执行 |
| 环境变量、别名、提示符 | .ksurc | 只影响交互式会话 |
| 安装二进制、放库文件 | 模块目录 | 需要持久化、随模块管理 |
| 会话级临时路径追加 | .ksurc | 依赖会话上下文 |
按这个表来分,基本不会放错地方。我见过有人把二进制文件直接丢在/data/local/tmp然后靠.ksurc加PATH,能用,但模块一升级或者清理临时目录就没了。正确做法是二进制放模块目录,.ksurc里把模块的 bin 目录加进PATH,两者各司其职。
5.3 调试技巧:怎么知道 .ksurc 到底执行了没
最直接的验证方式是在.ksurc末尾加一行日志:
echo "[ksurc] loaded at $(date)" >> /data/local/tmp/ksurc.log然后新开一个 su 会话,看日志文件有没有新增记录。有记录说明执行了,没记录说明要么路径不对、要么这个会话类型不加载.ksurc。这个方法比猜来猜去高效得多,建议养成习惯。
如果日志有了但某些配置没生效,那就是脚本内部逻辑问题,逐段注释排查即可。二分法在这里很好用:注释掉后半段,看前半段是否生效,逐步缩小范围。
6. 常见问题与排查实录
6.1 放了文件但不生效
这是最高频的问题。排查顺序建议如下:
- 确认路径:
ls -l /data/adb/ksu/.ksurc,文件真的在吗?文件名有没有拼错(注意前面那个点)? - 确认权限:属主是不是 root?权限是不是 600 或至少 root 可读?
- 确认会话类型:你是交互式
su进去的,还是su -c "cmd"?后者很多情况下不加载.ksurc。 - 确认 shell:你的
.ksurc语法在当前 shell 下能跑吗?加个日志行验证执行与否。 - 确认没有语法错误:手动
sh /data/adb/ksu/.ksurc跑一遍(注意这会污染当前 shell,测完记得退出),看有没有报错。
按这个顺序走,九成问题能定位。
6.2 su 进去就闪退或卡住
基本可以断定是.ksurc里有致命语句。常见元凶:
- 写了
exit,source 时直接终结 shell。 - 某条命令进入交互等待(比如没加参数的
read、等待输入的cat)。 - 死循环或者极慢的操作(比如对超大目录做
find)。 - 语法错误导致 shell 解析中断。
恢复办法前面说过,用su -c覆盖回备份,或者进 recovery 删文件。预防办法就是每次改动前备份、改动后先加日志验证。
6.3 环境变量"看起来设了但程序读不到"
这种情况多半不是.ksurc的问题,而是程序本身的运行上下文。比如你设了LD_LIBRARY_PATH,但目标程序是 setuid 的,或者被系统的链接器策略限制,那环境变量会被忽略。再比如某些应用通过自己的方式 fork 子进程,环境变量没继承过去。
排查思路:在.ksurc里export之后,用env | grep 变量名确认变量确实在当前 shell 里;然后直接在同一个 shell 里跑目标程序,看是否生效。如果 shell 里有、程序读不到,那就是程序侧的问题,跟.ksurc无关了。
6.4 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 文件放了没反应 | 路径错/权限错/非交互会话 | 核对路径权限,改用交互式 su 测试 |
| su 闪退 | 脚本含 exit 或阻塞命令 | 备份回滚,逐段注释排查 |
| 别名不生效 | 非交互式 shell | 别名仅对交互式有效,改用函数或脚本 |
| 颜色乱码 | shell 不支持该转义 | 改用纯文本提示符 |
| 变量设了程序不认 | 程序侧限制或未继承 | 在 shell 内直接验证,区分脚本问题与程序问题 |
| 每次 su 变慢 | 脚本里有耗时操作 | 移除网络请求、大目录扫描等 |
6.5 几条踩出来的经验
第一条:.ksurc里永远不要写需要人工交互的命令。source 执行意味着它和你的输入流是同一个,一个等待输入的命令会把整个会话卡死,而且从外面看就是"su 没反应",极难排查。
第二条:改动之后一定用新会话验证,不要在当前会话里 source 测试。当前会话 source 测试虽然能快速看语法,但它会污染你正在用的环境,测完一堆变量残留,反而干扰判断。新开会话最干净。
第三条:保持.ksurc短小。它的定位是会话初始化,不是万能脚本库。逻辑一多,出错概率直线上升,排查成本也高。复杂逻辑应该封装成独立脚本放模块目录,.ksurc里只留一行调用或者一个PATH追加。我现在的.ksurc稳定在二十行以内,够用,也好维护。
第四条:版本升级后重新验证。KernelSU 更新可能调整.ksurc的加载逻辑或默认 shell,升级完花两分钟新开个会话确认一下配置还在生效,比事后发现某个工具突然不能用了再回头查要省事得多。
这套东西说到底不复杂,核心就是搞清楚"会话级"和"系统级"的边界,然后在这个边界内把该设的设好、该避的避开。.ksurc本身只是个入口,真正有价值的是背后那套对执行时机和作用域的理解——把这个理清了,类似的定制需求放到别的环境里也能举一反三。