Linux fg命令

fg命令是Shell作业控制的关键命令,用于将后台作业或暂停的作业切换到前台运行。它与bgjobsCtrl+Z等命令配合使用,提供完整的进程控制能力。
注意:fg命令只能在交互式Shell中使用,且作业控制功能需要Shell支持。在脚本中使用时可能需要显式启用作业控制。

一、命令简介

fg命令(foreground的缩写)用于将后台运行的作业或暂停的作业恢复到前台继续执行。它是Linux/Unix Shell作业控制系统的核心组件,允许用户在多个任务间灵活切换,是交互式Shell环境中最重要的进程管理工具之一。

作业控制的核心命令:

  • jobs:列出当前Shell中的所有作业
  • fg:将作业切换到前台运行
  • bg:将作业切换到后台运行
  • Ctrl+Z:暂停当前前台作业(发送SIGTSTP信号)
  • Ctrl+C:终止当前前台作业(发送SIGINT信号)
  • disown:从作业表中移除作业

二、命令语法

fg [作业标识符]
fg %作业号

三、作业标识符

标识符 说明 示例
%n 作业号n(n为数字) fg %1fg %2
%+%% 当前作业(最近使用的作业) fg %+fg %%
%- 上一个作业 fg %-
%string 以string开头的作业 fg %vi(切换到以vi开头的作业)
%?string 包含string的作业 fg %?py(切换到包含py的作业)
%-- 无参数时默认使用当前作业 fg(等价于fg %+

四、作业状态转换

当前状态 执行fg 说明
Running (后台) Running (前台) 后台运行作业切换到前台继续运行
Stopped (后台) Running (前台) 暂停的后台作业切换到前台并继续运行
Stopped (前台) Running (前台) 暂停的前台作业恢复运行(不改变前后台状态)

五、使用示例

1. 基本用法

# 场景:将后台作业切换到前台

# 步骤1:启动一个后台作业
sleep 300 &
# [1] 12345

# 步骤2:查看作业状态
jobs
# [1]+  Running                 sleep 300 &

# 步骤3:将作业切换到前台
fg
# sleep 300
# 现在作业在前台运行,按Ctrl+C可终止

2. 多个作业管理

# 启动多个后台作业
sleep 1000 &
# [1] 12345

sleep 2000 &
# [2] 12346

# 查看所有作业
jobs
# [1]-  Running                 sleep 1000 &
# [2]+  Running                 sleep 2000 &

# 将作业2切换到前台
fg %2
# sleep 2000

# 按Ctrl+Z暂停
# [2]+  Stopped                 sleep 2000

# 将作业1切换到前台
fg %1
# sleep 1000

3. 编辑器任务切换

# 场景:在编辑器和编译任务间切换

# 启动vim编辑文件
vim document.txt
# 编辑中...

# 按Ctrl+Z暂停vim
# [1]+  Stopped                 vim document.txt

# 启动编译任务
make -j4
# 编译输出...

# 按Ctrl+Z暂停编译
# [2]+  Stopped                 make -j4

# 查看所有作业
jobs
# [1]-  Stopped                 vim document.txt
# [2]+  Stopped                 make -j4

# 切换回编辑器
fg %1
# 回到vim编辑器

# 保存文件后再次暂停
# :wq 或 Ctrl+Z

# 切换回编译任务
fg %2
# 继续编译

4. 下载任务管理

# 启动大文件下载
wget http://example.com/large.iso &
# [1] 12345

# 查看下载进度(后台作业)
jobs
# [1]+  Running                 wget http://example.com/large.iso &

# 如果需要查看详细输出,切换到前台
fg %1
# wget http://example.com/large.iso
# 显示下载进度...

# 按Ctrl+Z暂停下载
# [1]+  Stopped                 wget http://example.com/large.iso

# 做其他工作...

# 恢复下载(后台继续)
bg %1
# [1]+ wget http://example.com/large.iso &

# 再次切换到前台查看状态
fg %1
# 查看当前下载状态

5. 使用作业名称标识符

# 启动不同类型的作业
python data_analysis.py &
# [1] 12345

tail -f /var/log/syslog &
# [2] 12346

vim notes.txt &
# [3] 12347

# 查看所有作业
jobs
# [1]   Running                 python data_analysis.py &
# [2]-  Running                 tail -f /var/log/syslog &
# [3]+  Running                 vim notes.txt &

# 使用名称标识符切换
fg %python     # 切换到以python开头的作业
fg %?log       # 切换到包含log的作业
fg %vim        # 切换到以vim开头的作业

6. 数据库操作切换

# 场景:在数据库操作和脚本编辑间切换

# 启动MySQL客户端
mysql -u root -p
# 进入MySQL交互模式

# 执行长时间查询时按Ctrl+Z暂停
# [1]+  Stopped                 mysql -u root -p

# 编辑SQL脚本
vim query.sql
# 编写查询语句...

# 按Ctrl+Z暂停编辑器
# [2]+  Stopped                 vim query.sql

# 切换回MySQL客户端
fg %1
# 回到MySQL,执行刚才编写的查询
# mysql> source query.sql;

# 查询执行中按Ctrl+Z暂停
# [1]+  Stopped                 mysql -u root -p

# 切换到编辑器修改脚本
fg %2
# 修改query.sql...

7. 网络诊断任务切换

# 同时进行多个网络诊断
ping google.com > ping.log &
# [1] 12345

traceroute example.com > trace.log &
# [2] 12346

mtr yahoo.com > mtr.log &
# [3] 12347

# 查看所有诊断任务
jobs
# [1]   Running                 ping google.com > ping.log &
# [2]-  Running                 traceroute example.com > trace.log &
# [3]+  Running                 mtr yahoo.com > mtr.log &

# 切换到特定任务查看实时输出
fg %ping
# 查看ping的实时输出,按Ctrl+Z暂停

fg %traceroute
# 查看traceroute进度,按Ctrl+Z暂停

fg %mtr
# 查看mtr实时诊断,按Ctrl+C终止

8. 脚本调试场景

# 调试复杂脚本时的任务切换

# 在后台运行测试脚本
./test_script.sh &
# [1] 12345

# 监控日志文件
tail -f test.log &
# [2] 12346

# 编辑脚本文件
vim test_script.sh
# 发现bug,修改...

# 按Ctrl+Z暂停编辑
# [3]+  Stopped                 vim test_script.sh

# 查看测试输出
fg %2
# tail -f test.log
# 查看错误信息...

# 按Ctrl+Z暂停日志监控
# [2]+  Stopped                 tail -f test.log

# 回到编辑器修复bug
fg %3
# 修复bug后保存退出

# 重新运行测试
./test_script.sh &
# [4] 12347

# 监控新日志
fg %2
# 继续监控日志...

六、实用技巧

技巧1:智能作业切换脚本
#!/bin/bash
# smart_fg.sh - 智能作业切换工具

# 如果没有参数,尝试智能选择
if [ $# -eq 0 ]; then
    # 获取所有作业
    JOB_LIST=$(jobs -l)

    if [ -z "$JOB_LIST" ]; then
        echo "没有可用的作业"
        exit 1
    fi

    # 如果有多个作业,让用户选择
    if [ $(echo "$JOB_LIST" | wc -l) -gt 1 ]; then
        echo "可用的作业:"
        echo "$JOB_LIST"
        echo
        echo "请选择要切换到前台的作业:"
        read -p "作业号 (或名称): " JOB_SPEC

        if [[ "$JOB_SPEC" =~ ^[0-9]+$ ]]; then
            # 数字作业号
            fg %$JOB_SPEC
        else
            # 名称匹配
            fg %$JOB_SPEC
        fi
    else
        # 只有一个作业,直接切换
        fg
    fi
else
    # 使用提供的参数
    fg %$1
fi
技巧2:作业历史记录
#!/bin/bash
# job_history.sh - 作业历史记录和管理

HISTORY_FILE="$HOME/.job_history"
MAX_HISTORY=100

# 记录作业变化
record_job_change() {
    local action="$1"
    local job_info="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    echo "[$timestamp] $action: $job_info" >> "$HISTORY_FILE"

    # 限制历史记录大小
    if [ $(wc -l < "$HISTORY_FILE") -gt $MAX_HISTORY ]; then
        tail -n $MAX_HISTORY "$HISTORY_FILE" > "${HISTORY_FILE}.tmp"
        mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
    fi
}

# 包装fg命令
smart_fg() {
    # 获取当前作业信息
    local job_info=$(jobs -l | grep "^\[$1\]" 2>/dev/null)

    if [ -z "$job_info" ]; then
        # 尝试其他匹配方式
        job_info=$(jobs -l | grep "$1" | head -1)
    fi

    if [ -n "$job_info" ]; then
        record_job_change "FG" "$job_info"
        fg %$1
    else
        echo "未找到作业: $1"
        return 1
    fi
}

# 包装bg命令
smart_bg() {
    local job_info=$(jobs -l | grep "^\[$1\]" 2>/dev/null)

    if [ -z "$job_info" ]; then
        job_info=$(jobs -l | grep "$1" | head -1)
    fi

    if [ -n "$job_info" ]; then
        record_job_change "BG" "$job_info"
        bg %$1
    else
        echo "未找到作业: $1"
        return 1
    fi
}

# 查看历史
show_history() {
    if [ -f "$HISTORY_FILE" ]; then
        echo "作业历史记录:"
        cat "$HISTORY_FILE"
    else
        echo "暂无历史记录"
    fi
}

# 主函数
case "$1" in
    fg)
        shift
        smart_fg "$@"
        ;;
    bg)
        shift
        smart_bg "$@"
        ;;
    history)
        show_history
        ;;
    stats)
        echo "作业统计:"
        echo "当前作业数: $(jobs | wc -l)"
        if [ -f "$HISTORY_FILE" ]; then
            echo "历史记录数: $(wc -l < "$HISTORY_FILE")"
            echo "最近操作:"
            tail -5 "$HISTORY_FILE"
        fi
        ;;
    *)
        echo "用法: $0 {fg [作业]|bg [作业]|history|stats}"
        exit 1
        ;;
esac
技巧3:作业自动恢复系统
#!/bin/bash
# job_recovery.sh - 作业自动恢复系统

LOG_FILE="/tmp/job_recovery.log"
RECOVERY_INTERVAL=30

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 检查并恢复暂停的作业
check_and_recover_jobs() {
    # 获取所有暂停的作业
    local stopped_jobs=$(jobs -s)

    if [ -n "$stopped_jobs" ]; then
        log "发现暂停的作业:"
        log "$stopped_jobs"

        # 尝试自动恢复某些类型的作业
        echo "$stopped_jobs" | while read line; do
            local job_num=$(echo "$line" | sed -n 's/\[\([0-9]\+\)\].*/\1/p')
            local job_cmd=$(echo "$line" | cut -d' ' -f3-)

            # 根据命令类型决定是否自动恢复
            case "$job_cmd" in
                *download*|*wget*|*curl*|*rsync*)
                    log "恢复下载任务: $job_cmd"
                    bg %$job_num
                    ;;
                *make*|*compile*|*build*)
                    log "恢复编译任务: $job_cmd"
                    bg %$job_num
                    ;;
                *encode*|*convert*|*compress*)
                    log "恢复编码/转换任务: $job_cmd"
                    bg %$job_num
                    ;;
                *)
                    log "保持暂停状态: $job_cmd"
                    ;;
            esac
        done
    fi

    # 检查长时间运行的前台作业
    local running_jobs=$(jobs -r)
    if [ -n "$running_jobs" ]; then
        log "运行中的作业:"
        log "$running_jobs"
    fi
}

# 监控循环
monitor_loop() {
    log "作业监控系统启动"

    while true; do
        check_and_recover_jobs
        sleep "$RECOVERY_INTERVAL"
    done
}

# 启动监控(后台运行)
start_monitor() {
    monitor_loop &
    MONITOR_PID=$!
    log "监控进程PID: $MONITOR_PID"
    echo $MONITOR_PID > /tmp/job_monitor.pid
}

# 停止监控
stop_monitor() {
    if [ -f /tmp/job_monitor.pid ]; then
        local pid=$(cat /tmp/job_monitor.pid)
        kill $pid 2>/dev/null
        rm -f /tmp/job_monitor.pid
        log "监控已停止"
    else
        log "监控未运行"
    fi
}

# 主程序
case "$1" in
    start)
        start_monitor
        ;;
    stop)
        stop_monitor
        ;;
    status)
        if [ -f /tmp/job_monitor.pid ]; then
            local pid=$(cat /tmp/job_monitor.pid)
            if ps -p $pid > /dev/null; then
                log "监控运行中 (PID: $pid)"
            else
                log "监控进程已停止"
            fi
        else
            log "监控未运行"
        fi
        ;;
    check)
        check_and_recover_jobs
        ;;
    *)
        echo "用法: $0 {start|stop|status|check}"
        exit 1
        ;;
esac
技巧4:作业优先级管理
#!/bin/bash
# job_priority.sh - 作业优先级管理系统

# 作业优先级定义
declare -A PRIORITY_LEVELS=(
    ["high"]=0
    ["medium"]=1
    ["low"]=2
)

# 作业数据库
JOB_DB="$HOME/.job_priority.db"

# 初始化数据库
init_db() {
    if [ ! -f "$JOB_DB" ]; then
        cat > "$JOB_DB" << EOF
# 作业优先级数据库
# 格式: 作业标识|优先级|创建时间|最后访问
EOF
    fi
}

# 记录作业
record_job() {
    local job_id="$1"
    local priority="${2:-medium}"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    # 检查是否已存在
    if grep -q "^$job_id|" "$JOB_DB" 2>/dev/null; then
        # 更新最后访问时间
        sed -i "/^$job_id|/s/|[^|]*$/|$timestamp/" "$JOB_DB"
    else
        # 新增记录
        echo "$job_id|$priority|$timestamp|$timestamp" >> "$JOB_DB"
    fi
}

# 获取作业优先级
get_job_priority() {
    local job_id="$1"
    local priority=$(grep "^$job_id|" "$JOB_DB" 2>/dev/null | cut -d'|' -f2)
    echo "${priority:-medium}"
}

# 智能fg:根据优先级选择作业
priority_fg() {
    init_db

    # 获取当前作业
    local jobs_list=$(jobs -l)

    if [ -z "$jobs_list" ]; then
        echo "没有可用的作业"
        return 1
    fi

    # 解析作业信息
    declare -a HIGH_JOBS
    declare -a MEDIUM_JOBS
    declare -a LOW_JOBS

    while IFS= read -r line; do
        local job_num=$(echo "$line" | sed -n 's/\[\([0-9]\+\)\].*/\1/p')
        local job_pid=$(echo "$line" | awk '{print $2}')
        local job_cmd=$(echo "$line" | cut -d' ' -f4-)

        # 获取优先级
        local priority=$(get_job_priority "$job_num")

        # 记录到相应数组
        case "$priority" in
            high)
                HIGH_JOBS+=("$job_num:$job_cmd")
                ;;
            medium)
                MEDIUM_JOBS+=("$job_num:$job_cmd")
                ;;
            low)
                LOW_JOBS+=("$job_num:$job_cmd")
                ;;
        esac

        # 更新访问记录
        record_job "$job_num" "$priority"

    done <<< "$jobs_list"

    # 按优先级顺序选择作业
    local selected_job

    if [ ${#HIGH_JOBS[@]} -gt 0 ]; then
        echo "高优先级作业:"
        for job in "${HIGH_JOBS[@]}"; do
            echo "  ${job//:/ - }"
        done
        selected_job=$(echo "${HIGH_JOBS[0]}" | cut -d: -f1)
    elif [ ${#MEDIUM_JOBS[@]} -gt 0 ]; then
        echo "中优先级作业:"
        for job in "${MEDIUM_JOBS[@]}"; do
            echo "  ${job//:/ - }"
        done
        selected_job=$(echo "${MEDIUM_JOBS[0]}" | cut -d: -f1)
    elif [ ${#LOW_JOBS[@]} -gt 0 ]; then
        echo "低优先级作业:"
        for job in "${LOW_JOBS[@]}"; do
            echo "  ${job//:/ - }"
        done
        selected_job=$(echo "${LOW_JOBS[0]}" | cut -d: -f1)
    fi

    if [ -n "$selected_job" ]; then
        echo "切换到作业: $selected_job"
        fg %$selected_job
    fi
}

# 设置作业优先级
set_priority() {
    init_db

    local job_id="$1"
    local priority="$2"

    if [ -z "$job_id" ] || [ -z "$priority" ]; then
        echo "用法: $0 set <作业号> "
        return 1
    fi

    if [[ ! "${!PRIORITY_LEVELS[@]}" =~ $priority ]]; then
        echo "无效的优先级: $priority"
        return 1
    fi

    record_job "$job_id" "$priority"
    echo "作业 $job_id 的优先级已设置为 $priority"
}

# 查看作业优先级
show_priorities() {
    init_db

    echo "作业优先级列表:"
    echo "作业号 | 优先级 | 创建时间 | 最后访问"
    echo "-------------------------------------"

    # 获取当前所有作业
    local current_jobs=$(jobs -l | awk '{print $1}' | tr -d '[]+')

    for job in $current_jobs; do
        local record=$(grep "^$job|" "$JOB_DB" 2>/dev/null)
        if [ -n "$record" ]; then
            echo "$record" | tr '|' ' '
        else
            echo "$job | medium | (未记录) | (未记录)"
        fi
    done
}

# 主程序
case "$1" in
    fg)
        priority_fg
        ;;
    set)
        shift
        set_priority "$@"
        ;;
    show)
        show_priorities
        ;;
    *)
        echo "用法: $0 {fg|set <作业> <优先级>|show}"
        exit 1
        ;;
esac

七、fg vs bg vs & 对比

命令/操作 功能 常用场景 工作流程
fg 将作业切换到前台运行 需要与作业交互、查看输出、输入数据 后台/暂停 → 前台运行
bg 将作业切换到后台运行 让作业在后台继续执行,不占用终端 暂停 → 后台运行
& 启动时直接后台运行 不需要交互的长时间任务 直接后台运行
Ctrl+Z 暂停前台作业 临时中断作业去做其他事 前台运行 → 暂停
Ctrl+C 终止前台作业 需要立即停止作业 前台运行 → 终止

八、作业生命周期管理

典型作业生命周期:
  1. 创建:使用命令 &或直接运行命令
  2. 暂停:按Ctrl+Z发送SIGTSTP信号
  3. 后台恢复:使用bg命令发送SIGCONT信号
  4. 前台恢复:使用fg命令发送SIGCONT信号
  5. 交互:在前台运行时与作业交互
  6. 终止:按Ctrl+C或使用kill命令
  7. 清理:使用disown从作业表移除

九、注意事项

  • 交互式限制:fg命令只在交互式Shell中有效,脚本中默认禁用作业控制
  • 终端关联:前台作业与终端紧密关联,终端关闭会发送SIGHUP信号
  • 输入输出:前台作业可以接收终端输入,后台作业无法接收终端输入
  • 信号处理:不同程序对信号的处理方式不同,有些可能忽略SIGTSTP
  • Shell差异:不同Shell(bash、zsh、fish)的作业控制实现略有差异
  • 脚本作业控制:脚本中启用作业控制需使用set -m
  • 网络程序:网络连接的程序在前后台切换时可能断开连接
  • GUI程序:图形界面程序通常不应在Shell中控制,可能导致界面问题
  • 资源占用:前台作业会占用终端,阻塞其他Shell命令执行
  • 作业限制:系统对作业数量有限制,可用ulimit -u查看

十、常见问题

如果fg命令提示找不到作业,可能有以下几种原因和解决方法:

# 1. 使用jobs命令查看当前作业
jobs
# 输出示例:
# [1]   Running                 sleep 1000 &
# [2]-  Stopped                 vim file.txt

# 2. 检查作业号是否正确
# 错误的作业号
fg %99  # 如果作业99不存在

# 正确的作业号
fg %1   # 使用jobs显示的作业号

# 3. 作业可能已经终止
# 如果作业已经完成或被杀死,会从作业表中移除

# 4. 在不同的Shell中
# 作业是Shell特定的,每个Shell有自己的作业表
# 在一个Shell中启动的作业不能在另一个Shell中控制

# 5. 脚本中默认禁用作业控制
# 在脚本中使用需要显式启用
set -m  # 启用作业控制

# 6. 使用完整的作业标识符
fg      # 切换到当前作业(最近使用的作业)
fg %%   # 同上,切换到当前作业
fg %+   # 同上
fg %-   # 切换到上一个作业

# 7. 使用名称匹配
fg %sleep    # 切换到以sleep开头的作业
fg %?txt     # 切换到包含txt的作业

# 8. 作业可能被disown了
# disown命令会从作业表中移除作业
# 被disown的作业无法用fg/bg控制

# 9. 检查Shell是否支持作业控制
echo $-
# 如果输出包含"m",则支持作业控制

# 10. 完整的作业查找脚本
find_job() {
    local pattern="$1"

    # 使用jobs查找
    local job_info=$(jobs -l | grep -i "$pattern")

    if [ -z "$job_info" ]; then
        echo "未找到匹配的作业: $pattern"
        return 1
    fi

    echo "找到作业:"
    echo "$job_info"

    # 提取作业号
    local job_num=$(echo "$job_info" | head -1 | sed -n 's/\[\([0-9]\+\)\].*/\1/p')

    if [ -n "$job_num" ]; then
        echo "切换到作业 $job_num"
        fg %$job_num
    fi
}

# 使用示例
find_job "sleep"
find_job "vim"

有几种方法可以精确切换到特定的后台作业:

# 方法1: 使用作业号(最精确)
# 先查看所有作业及其编号
jobs -l
# [1]   12345 Running                 sleep 1000 &
# [2]-  12346 Stopped                 vim file.txt
# [3]+  12347 Running                 tail -f log.txt &

# 切换到作业2
fg %2

# 方法2: 使用进程ID(需要额外步骤)
# 查找特定进程的作业号
find_job_by_pid() {
    local pid="$1"
    local job_info=$(jobs -l | grep " $pid ")

    if [ -n "$job_info" ]; then
        local job_num=$(echo "$job_info" | sed -n 's/\[\([0-9]\+\)\].*/\1/p')
        echo "找到作业: $job_num (PID: $pid)"
        fg %$job_num
    else
        echo "未找到PID为 $pid 的作业"
    fi
}

# 使用示例
find_job_by_pid 12346

# 方法3: 使用命令名匹配
# 切换到以特定字符串开头的作业
fg %sleep     # 切换到以sleep开头的作业
fg %vim       # 切换到以vim开头的作业

# 切换到包含特定字符串的作业
fg %?file     # 切换到包含file的作业
fg %?log      # 切换到包含log的作业

# 方法4: 使用作业状态过滤
# 只切换到运行中的作业
fg_running() {
    local running_jobs=$(jobs -r)

    if [ -z "$running_jobs" ]; then
        echo "没有运行中的作业"
        return 1
    fi

    # 如果有多个运行中的作业,选择第一个
    local job_num=$(echo "$running_jobs" | head -1 | sed -n 's/\[\([0-9]\+\)\].*/\1/p')
    fg %$job_num
}

# 只切换到暂停的作业
fg_stopped() {
    local stopped_jobs=$(jobs -s)

    if [ -z "$stopped_jobs" ]; then
        echo "没有暂停的作业"
        return 1
    fi

    local job_num=$(echo "$stopped_jobs" | head -1 | sed -n 's/\[\([0-9]\+\)\].*/\1/p')
    fg %$job_num
}

# 方法5: 交互式选择
interactive_fg() {
    # 获取作业列表
    local job_list=$(jobs -l)

    if [ -z "$job_list" ]; then
        echo "没有可用的作业"
        return 1
    fi

    # 显示作业菜单
    echo "请选择要切换到前台的作业:"
    echo "$job_list" | cat -n

    # 读取用户选择
    read -p "请输入编号: " choice

    # 提取作业号
    local job_line=$(echo "$job_list" | sed -n "${choice}p")
    local job_num=$(echo "$job_line" | sed -n 's/\[\([0-9]\+\)\].*/\1/p')

    if [ -n "$job_num" ]; then
        fg %$job_num
    else
        echo "无效的选择"
    fi
}

# 方法6: 最近使用的作业
# fg(无参数)切换到当前作业(最近使用的作业)
fg
# 或
fg %+
fg %%

# 上一个作业
fg %-

脚本中默认禁用作业控制,需要显式启用:

#!/bin/bash
# fg_in_script.sh

# 方法1: 启用作业控制
set -m  # 启用作业控制
# 或
set -o monitor

echo "作业控制已启用"

# 启动后台作业
sleep 100 &
JOB1_PID=$!
echo "后台作业1 PID: $JOB1_PID"

sleep 200 &
JOB2_PID=$!
echo "后台作业2 PID: $JOB2_PID"

# 查看作业
jobs -l

# 将作业1切换到前台
fg %1
# 注意:这会使脚本暂停,直到作业1完成或再次被暂停

# 如果作业在前台运行,脚本会阻塞在这里
# 可以按Ctrl+Z暂停,然后脚本继续执行

echo "脚本继续执行..."

# 方法2: 使用子Shell启用作业控制
(
    echo "在子Shell中启用作业控制"
    set -m

    # 启动作业
    sleep 300 &

    # 查看作业
    jobs -l

    # 切换到前台
    fg %1

    echo "子Shell中的作业完成"
)

# 方法3: 使用协进程(coproc)
echo "使用协进程:"
coproc MYPROC {
    echo "协进程开始"
    sleep 5
    echo "协进程结束"
}

# 获取协进程信息
echo "协进程PID: ${MYPROC_PID}"
echo "输入FD: ${MYPROC[0]}"
echo "输出FD: ${MYPROC[1]}"

# 读取协进程输出
read -r output <&"${MYPROC[0]}"
echo "协进程输出: $output"

# 方法4: 使用命名管道控制
CONTROL_PIPE="/tmp/control_pipe.$$"
mkfifo "$CONTROL_PIPE"

# 启动受控进程
(
    echo "受控进程启动"

    # 从管道读取控制命令
    while read -r cmd; do
        case "$cmd" in
            "fg")
                echo "切换到前台模式"
                # 实际的前台处理逻辑
                ;;
            "bg")
                echo "切换到后台模式"
                ;;
            "pause")
                echo "暂停"
                ;;
            "resume")
                echo "恢复"
                ;;
            "exit")
                echo "退出"
                break
                ;;
        esac
    done < "$CONTROL_PIPE"
) &
CONTROLLED_PID=$!

# 发送控制命令
echo "fg" > "$CONTROL_PIPE"
sleep 1
echo "pause" > "$CONTROL_PIPE"
sleep 1
echo "resume" > "$CONTROL_PIPE"
sleep 1
echo "exit" > "$CONTROL_PIPE"

# 等待进程结束
wait $CONTROLLED_PID

# 清理
rm -f "$CONTROL_PIPE"

# 方法5: 使用文件锁和状态文件
STATE_FILE="/tmp/job_state.$$"

# 启动工作进程
(
    # 初始化状态
    echo "running" > "$STATE_FILE"

    trap 'echo "paused" > "$STATE_FILE"; exit 0' SIGTSTP
    trap 'echo "terminated" > "$STATE_FILE"; exit 0' SIGINT

    echo "工作进程运行中..."
    sleep 10
    echo "工作进程完成"
) &
WORKER_PID=$!

# 控制循环
while true; do
    # 读取状态
    state=$(cat "$STATE_FILE" 2>/dev/null || echo "unknown")

    case "$state" in
        "running")
            echo "工作进程运行中"
            ;;
        "paused")
            echo "工作进程已暂停"
            read -p "恢复执行? (y/n): " choice
            if [[ "$choice" == "y" ]]; then
                kill -CONT $WORKER_PID
                echo "running" > "$STATE_FILE"
            fi
            ;;
        "terminated")
            echo "工作进程已终止"
            break
            ;;
        *)
            echo "未知状态: $state"
            break
            ;;
    esac

    sleep 2
done

rm -f "$STATE_FILE"

脚本中使用fg的注意事项:

  • 作业控制会使脚本行为复杂化
  • 生产环境脚本通常避免使用作业控制
  • 考虑使用更专业的进程管理工具
  • 确保正确处理信号和清理资源
  • 注意不同Shell的兼容性问题

fg和Ctrl+Z组合使用可以实现强大的作业控制:

# 技巧1: 快速任务切换
# 启动任务A
vim file1.txt

# 按Ctrl+Z暂停
# [1]+  Stopped                 vim file1.txt

# 启动任务B
python script.py

# 按Ctrl+Z暂停
# [2]+  Stopped                 python script.py

# 快速切换回任务A
fg %1
# 或 fg %vim

# 技巧2: 临时中断处理
# 长时间运行的任务
./long_running_task.sh

# 运行中需要临时处理其他事情
# 按Ctrl+Z暂停
# [1]+  Stopped                 ./long_running_task.sh

# 处理临时任务
quick_check.sh

# 返回原任务
fg
# 或 fg %1

# 技巧3: 多任务轮转
# 启动多个任务
task1.sh &
task2.sh &
task3.sh &

# 轮流切换到前台查看状态
fg %1
# 查看任务1,按Ctrl+Z暂停

fg %2
# 查看任务2,按Ctrl+Z暂停

fg %3
# 查看任务3,按Ctrl+Z暂停

# 技巧4: 安全保存状态
# 重要编辑任务
vim important_doc.txt

# 临时需要离开,安全保存
# 在vim中按Ctrl+Z(保存文件并暂停vim)
# [1]+  Stopped                 vim important_doc.txt

# 可以安全关闭终端,稍后恢复
# 重新打开终端后,如果有作业会提示
# 有停止的作业

# 恢复编辑
fg
# 或 vim important_doc.txt

# 技巧5: 调试循环
debug_cycle() {
    # 启动被调试的程序
    ./buggy_program &
    DEBUG_PID=$!

    while true; do
        # 等待程序运行
        sleep 2

        # 暂停程序进行调试
        kill -TSTP $DEBUG_PID

        # 交互调试
        echo "程序已暂停,进行调试..."
        read -p "调试命令 (c=继续, q=退出): " cmd

        case "$cmd" in
            c)
                # 继续执行
                kill -CONT $DEBUG_PID
                ;;
            q)
                # 退出调试
                kill -TERM $DEBUG_PID
                break
                ;;
            *)
                echo "未知命令"
                ;;
        esac
    done
}

# 技巧6: 资源监控模式
resource_monitor() {
    # 启动资源密集型任务
    ./resource_hungry_task.sh &
    TASK_PID=$!

    # 监控循环
    while kill -0 $TASK_PID 2>/dev/null; do
        # 暂停任务进行检查
        kill -TSTP $TASK_PID

        # 检查资源使用
        echo "=== 资源检查 ==="
        ps -p $TASK_PID -o pid,%cpu,%mem,cmd
        echo

        # 等待用户决定
        read -p "继续执行? (y/n/p=暂停): " choice

        case "$choice" in
            y)
                # 继续执行
                kill -CONT $TASK_PID
                sleep 5  # 运行5秒后再检查
                ;;
            n)
                # 终止任务
                kill -TERM $TASK_PID
                break
                ;;
            p)
                # 保持暂停状态
                echo "任务保持暂停状态"
                break
                ;;
        esac
    done
}

# 技巧7: 批量作业控制
batch_control() {
    # 启动一批作业
    for i in {1..5}; do
        ./job_$i.sh &
        echo "作业 $i 启动: $!"
    done

    # 批量控制
    while true; do
        echo "当前作业状态:"
        jobs -l

        echo
        echo "控制选项:"
        echo "1. 将所有作业切换到前台"
        echo "2. 将所有作业暂停"
        echo "3. 恢复所有暂停的作业"
        echo "4. 终止所有作业"
        echo "5. 退出"

        read -p "选择: " option

        case "$option" in
            1)
                # 将所有作业依次切换到前台
                for job in $(jobs -p); do
                    fg %$(jobs -l | grep " $job " | sed 's/\[\([0-9]\+\).*/\1/')
                    # 按Ctrl+Z暂停当前作业,继续下一个
                done
                ;;
            2)
                # 暂停所有运行中的作业
                for job in $(jobs -rp); do
                    kill -TSTP $job
                done
                ;;
            3)
                # 恢复所有暂停的作业
                for job in $(jobs -sp); do
                    kill -CONT $job
                done
                ;;
            4)
                # 终止所有作业
                for job in $(jobs -p); do
                    kill -TERM $job
                done
                break
                ;;
            5)
                break
                ;;
        esac
    done
}

十一、最佳实践

fg命令使用最佳实践
  1. 确认作业状态:使用jobs -l查看作业详情后再操作
  2. 使用作业标识符:优先使用作业号(%n)而不是进程ID,更稳定可靠
  3. 输出管理:后台作业的输出应重定向,避免切换时干扰
  4. 会话管理:长时间作业使用screentmux管理
  5. 资源监控:切换作业前检查系统资源使用情况
  6. 信号处理:了解SIGTSTP、SIGCONT等信号对作业的影响
  7. 作业清理:完成后的作业及时终止或从作业表移除
  8. 脚本谨慎:生产环境脚本避免使用作业控制,使用专业进程管理
  9. 终端安全:重要作业确保终端连接稳定或使用nohup
  10. 记录操作:重要的作业切换操作应有记录,便于问题排查