使用sudo命令执行程序不保留父进程
1. 背景
程序使用Jenkins发布,启动用户是普通用户。有时会在服务器上直接重启程序,使用“sudo -u 普通用户 程序”执行时,会产生两个进程一个是"sudo -u …"的父进程,一个是“程序”的子进程。为了不混淆研发,所以修改重启脚本,执行后只保留子进程。
2. 现象
- 普通用户执行sleep命令
sudo -u control sleep 20
- 查看sleep进程
# 查看命令
ps -ef | grep sleep | grep -v grep
# 看到两个进程
root 20043 19956 0 10:42 pts/0 00:00:00 sudo -u control sleep 20
control 20044 20043 0 10:42 pts/0 00:00:00 sleep 20
- 父进程是root执行的,进程ID=20043
- 子进程是普通用户control执行的,父进程ID=20043
3. 解决
- 使用bash -c和disown解决
sudo -u control bash -c "sleep 20 & disown"
- 查看sleep进程
# 查看命令
ps -ef | grep sleep | grep -v grep
# 看到只有1个进程
control 20167 1 0 10:54 pts/0 00:00:00 sleep 20
4. 说明
bash -c 结合 disown 是一种在 Linux 中启动后台进程并使其与当前 shell 分离的有效方法
4.1 基本概念
-
bash -c 命令
bash -c “command” 允许您直接在命令行中执行一段 Bash 脚本,而不需要先创建一个脚本文件。 -
disown 命令
disown 是 Bash 的内置命令,用于从当前 shell 的作业表中移除作业,使它们不再接收 SIGHUP 信号(当终端关闭时发送的信号)。
4.2 工作原理
使用 bash -c “command & disown” 时:
- bash -c 启动一个新的 Bash 子 shell
- 在这个子 shell 中,command & 将命令置于后台运行
- disown 立即将这个后台作业从子 shell 的作业表中移除
- 子 shell 退出,但后台进程继续运行,其父进程变为 init (PID 1)
5. 样例
- 脚本目录结构
[root@centos7 kol-backend]# tree
.
├── bak
├── lib
├── script
│ └── restart.sh
└── sourcecode
- kol-backend:程序主目录
- bak子目录:启动成功后备份目录;
- lib子目录: Jenkins推送压缩包目录;
- script子目录:重启脚本目录;
- sourcecode子目录:程序运行目录;
注意:程序主目录及子目录的属主都是程序运行的普通用户
- 重启脚本
cat restart.sh
#!/bin/bash
#######################################################
# 脚本用于重启程序
# 使用前需要定义程序路径和变量
# 使用前需要修改启动程序命令,例如java路径和启动的普通账号
######################################################## 定义程序路径和变量
APP_HOME="/public/application/kol-backend"
APP_NAME="kol-job.jar"
CONF_NAME="shared-config.tar.gz"
ENV_NAME="kol-prod"
# lib子目录: Jenkins推送压缩包目录; bak子目录:启动成功后备份目录; sourcecode子目录:运行程序目录; script子目录:重启脚本目录# 当前登录用户
CURRENT_USER=$(id -un)
# 获取当前时间
CURRENT_TIME=$(date +"%Y%m%d%H%M%S")
# 当前目录下创建脚本日志目录
mkdir -p logs# 获取脚本名称
SCRIPT_FULL_NAME=$(basename "$0")
# 移除最后一个点及其后的所有字符
SCRIPT_NAME="${SCRIPT_FULL_NAME%.*}"
# 定义脚本日志文件
LOG_FILE=logs/${SCRIPT_NAME}_${CURRENT_TIME}.logDATE_TIME=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "\n $DATE_TIME: #### Begin ####" >> $LOG_FILE# 1. 使用root账号停服务
# 获取运行程序进程的PID
PID=$(sudo ps -ef | grep "$APP_HOME/sourcecode/$APP_NAME" | grep -v grep | awk '{print $2}');
# 将获取的旧进程PID结果写入日志
[ -n "$PID" ] && echo -e "\n $APP_NAME OLD $PID">>$LOG_FILE || echo -e "\n $APP_NAME PID为空">>$LOG_FILE;
# 正常停止进程
[ -n "$PID" ] && sudo kill $PID && echo -e "\n $APP_NAME 开始停止">>$LOG_FILE;# 2. 检查进程PID是否存在,30秒后强杀
# 连续30秒检查进程PID是否存在,PID不存在退出循环
[ -n "$PID" ] && for i in {1..30}; do sudo kill -0 $PID 1>/dev/null || break; sleep 1; done;
# 超过30秒后,仍然存在进程PID,启动强杀
sudo kill -0 $PID 2>/dev/null && sudo kill -9 $PID >/dev/null && echo -e "\n $APP_NAME 启用强杀">>$LOG_FILE
sudo sleep 10
# 检查进程是否停止,写入日志(这里没考虑强杀不成功的情况)
sudo kill -0 $PID 2>/dev/null || echo -e "\n $APP_NAME 停止成功">>$LOG_FILE# 3. 当前用户启动APP程序
# 进入程序目录,使用普通用户启动程序(bash -c 和 disown)
cd $APP_HOME/sourcecode && sudo -u control bash -c "nohup /usr/local/jvm/java21/bin/java -Xms16g -Xmx32g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.hprof -Dspring.profiles.active=$ENV_NAME -Dspring.config.additional-location=file:$APP_HOME/sourcecode/shared-config/ -jar $APP_HOME/sourcecode/$APP_NAME > $APP_HOME/sourcecode/nohup.out 2>&1 & disown"
sudo sleep 20# 4. 判断是否启动成功
PID=$(sudo ps -ef | grep "$APP_HOME/sourcecode/$APP_NAME" | grep -v grep | awk '{print $2}'); # 5. 如果成功备份jar包
if [ -n "$PID" ]; then# 新进程PID写入日志echo -e "\n $APP_NAME NEW $PID">>$LOG_FILE;echo -e "\n $APP_HOME 启动成功">>$LOG_FILE;
else echo -e "\n $APP_HOME 启动失败">>$LOG_FILE;
fiDATE_TIME=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "\n $DATE_TIME: #### Finish ####" >>$LOG_FILE