强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Bash 脚本编写教程 / 16 - 系统管理

16 - 系统管理

16.1 用户与权限管理

用户信息查询

# 当前用户信息
echo "用户名: $(whoami)"
echo "用户ID: $(id -u)"
echo "组ID:   $(id -g)"
echo "所有组: $(id -Gn)"

# 用户列表
awk -F: '{printf "%-20s UID=%-6s GID=%-6s %s\n", $1, $3, $4, $7}' /etc/passwd | head -10

# 登录用户
who
w

# 最近登录
last | head -10

# sudo 日志
journalctl _COMM=sudo --no-pager | tail -20

批量用户管理

#!/bin/bash
# user_mgmt.sh —— 批量用户管理工具
set -euo pipefail

readonly GROUP="developers"

create_user() {
    local username="$1"
    local fullname="$2"
    local shell="${3:-/bin/bash}"

    # 检查用户是否已存在
    if id "$username" &>/dev/null; then
        echo "⚠️  用户 $username 已存在"
        return 0
    fi

    # 生成随机密码
    local password
    password=$(openssl rand -base64 12)

    # 创建用户
    useradd -m -c "$fullname" -s "$shell" -G "$GROUP" "$username"
    echo "$username:$password" | chpasswd

    # 强制首次登录修改密码
    chage -d 0 "$username"

    echo "✅ 创建用户: $username"
    echo "   初始密码: $password"
    echo "   全名: $fullname"
    echo "   组: $GROUP"
}

disable_user() {
    local username="$1"

    if ! id "$username" &>/dev/null; then
        echo "❌ 用户 $username 不存在" >&2
        return 1
    fi

    # 锁定账户
    passwd -l "$username"
    # 禁用登录 Shell
    chsh -s /usr/sbin/nologin "$username"
    # 终止所有进程
    pkill -u "$username" 2>/dev/null || true

    echo "🔒 已禁用用户: $username"
}

delete_user() {
    local username="$1"
    local keep_home="${2:-no}"

    if ! id "$username" &>/dev/null; then
        echo "❌ 用户 $username 不存在" >&2
        return 1
    fi

    # 终止进程
    pkill -u "$username" 2>/dev/null || true
    sleep 1

    if [[ "$keep_home" == "yes" ]]; then
        userdel "$username"
    else
        userdel -r "$username"
    fi

    echo "🗑️  已删除用户: $username"
}

# 从 CSV 文件批量创建
batch_create() {
    local csv_file="$1"
    # CSV 格式: username,fullname,shell

    while IFS=, read -r username fullname shell; do
        [[ -z "$username" || "$username" == \#* ]] && continue
        create_user "$username" "$fullname" "${shell:-/bin/bash}"
    done < "$csv_file"
}

# 用法
case "${1:-help}" in
    create)    create_user "$2" "$3" "${4:-/bin/bash}" ;;
    disable)   disable_user "$2" ;;
    delete)    delete_user "$2" "${3:-no}" ;;
    batch)     batch_create "$2" ;;
    *)
        echo "用法: $0 {create|disable|delete|batch} [参数...]"
        echo "  create <用户名> <全名> [shell]"
        echo "  disable <用户名>"
        echo "  delete <用户名> [保留主目录:yes/no]"
        echo "  batch <csv文件>"
        ;;
esac

16.2 服务管理

#!/bin/bash
# service_mgr.sh —— 服务管理工具
set -euo pipefail

readonly SERVICE_NAME="${1:?用法: $0 <服务名> {start|stop|restart|status|logs|enable|disable}}"

# 检查 systemd 是否可用
if ! command -v systemctl &>/dev/null; then
    echo "系统不使用 systemd" >&2
    exit 1
fi

case "${2:-status}" in
    start)
        echo "启动服务: $SERVICE_NAME"
        systemctl start "$SERVICE_NAME"
        systemctl status "$SERVICE_NAME" --no-pager
        ;;
    stop)
        echo "停止服务: $SERVICE_NAME"
        systemctl stop "$SERVICE_NAME"
        ;;
    restart)
        echo "重启服务: $SERVICE_NAME"
        systemctl restart "$SERVICE_NAME"
        sleep 2
        systemctl status "$SERVICE_NAME" --no-pager
        ;;
    status)
        systemctl status "$SERVICE_NAME" --no-pager
        ;;
    logs)
        journalctl -u "$SERVICE_NAME" -f --no-pager
        ;;
    enable)
        systemctl enable "$SERVICE_NAME"
        echo "✅ 服务已设置为开机自启"
        ;;
    disable)
        systemctl disable "$SERVICE_NAME"
        echo "✅ 服务已取消开机自启"
        ;;
    *)
        echo "未知命令: ${2}" >&2
        exit 1
        ;;
esac

16.3 磁盘与文件系统

# 磁盘使用情况
df -h

# 指定挂载点
df -h / /home /var

# 目录大小
du -sh /var/log

# 最大的文件/目录
du -ah /var 2>/dev/null | sort -rh | head -20

# 查找大文件
find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh | head -20

# 查找并清理日志
find /var/log -name "*.log" -mtime +30 -exec ls -lh {} \;
find /var/log -name "*.log" -mtime +30 -delete

# inode 使用情况
df -i

# 挂载点信息
findmnt -t ext4,xfs

# 磁盘健康
smartctl -a /dev/sda 2>/dev/null || echo "需要安装 smartmontools"

16.4 进程管理

# 查看进程
ps aux | head -20

# 按 CPU 排序
ps aux --sort=-%cpu | head -10

# 按内存排序
ps aux --sort=-%mem | head -10

# 查找特定进程
ps aux | grep -E "[n]ginx"  # 使用 [] 技巧排除 grep 自身

# 进程树
pstree -p

# 实时监控
top -bn1 | head -20

# 查看进程的打开文件
lsof -p $$ | head -20

# 查看端口占用
ss -tlnp | head -20
netstat -tlnp 2>/dev/null | head -20

# 杀死进程
kill PID                    # 发送 SIGTERM
kill -9 PID                 # 强制杀死 (SIGKILL)
pkill -f "pattern"          # 按名称杀死

# 查找占用端口的进程
fuser 80/tcp 2>/dev/null
lsof -i :80 2>/dev/null

16.5 系统备份脚本

#!/bin/bash
# backup.sh —— 系统备份脚本
set -euo pipefail

readonly BACKUP_DIR="/backup"
readonly DATE=$(date +%Y%m%d_%H%M%S)
readonly RETENTION_DAYS=30
readonly LOG_FILE="/var/log/backup.log"

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

# 创建备份目录
mkdir -p "$BACKUP_DIR"/{daily,weekly,monthly}

# 备份数据库
backup_database() {
    local db_name="$1"
    local backup_file="$BACKUP_DIR/daily/${db_name}_${DATE}.sql.gz"
    
    log "备份数据库: $db_name"
    
    mysqldump --single-transaction --routines --triggers "$db_name" | \
        gzip > "$backup_file"
    
    local size
    size=$(du -h "$backup_file" | cut -f1)
    log "数据库备份完成: $backup_file ($size)"
}

# 备份文件目录
backup_files() {
    local source="$1"
    local name=$(basename "$source")
    local backup_file="$BACKUP_DIR/daily/${name}_${DATE}.tar.gz"
    
    log "备份目录: $source"
    
    tar czf "$backup_file" "$source" 2>/dev/null
    
    local size
    size=$(du -h "$backup_file" | cut -f1)
    log "文件备份完成: $backup_file ($size)"
}

# 清理旧备份
cleanup_old_backups() {
    log "清理 ${RETENTION_DAYS} 天前的备份..."
    
    local count=0
    while IFS= read -r file; do
        rm -f "$file"
        ((count++))
    done < <(find "$BACKUP_DIR" -type f -mtime +$RETENTION_DAYS -name "*.gz")
    
    log "清理完成,删除 $count 个文件"
}

# 验证备份完整性
verify_backup() {
    local file="$1"
    
    if [[ ! -f "$file" ]]; then
        log "❌ 备份文件不存在: $file"
        return 1
    fi
    
    if [[ ! -s "$file" ]]; then
        log "❌ 备份文件为空: $file"
        return 1
    fi
    
    if file "$file" | grep -q "gzip"; then
        if gzip -t "$file" 2>/dev/null; then
            log "✅ 备份完整性验证通过: $file"
            return 0
        fi
    fi
    
    log "❌ 备份文件损坏: $file"
    return 1
}

# 主函数
main() {
    log "========================================"
    log "备份开始"
    log "========================================"
    
    # 执行备份
    backup_database "myapp"
    backup_files "/etc"
    backup_files "/home"
    backup_files "/opt/myapp"
    
    # 验证
    verify_backup "$BACKUP_DIR/daily/myapp_${DATE}.sql.gz"
    
    # 清理
    cleanup_old_backups
    
    log "========================================"
    log "备份完成"
    log "========================================"
}

main "$@"

16.6 cron 集成

cron 基础

# crontab 格式:
# ┌──── 分钟 (0-59)
# │ ┌──── 小时 (0-23)
# │ │ ┌──── 日 (1-31)
# │ │ │ ┌──── 月 (1-12)
# │ │ │ │ ┌──── 星期 (0-7, 0和7都是周日)
# │ │ │ │ │
# * * * * * command

# 查看 crontab
crontab -l

# 编辑 crontab
crontab -e

# 常用 cron 表达式
# 每天凌晨 2 点
# 0 2 * * * /opt/scripts/backup.sh

# 每 5 分钟
# */5 * * * * /opt/scripts/monitor.sh

# 工作日每天 9 点
# 0 9 * * 1-5 /opt/scripts/report.sh

# 每月 1 号
# 0 0 1 * * /opt/scripts/monthly_cleanup.sh

cron 脚本模板

#!/bin/bash
# cron_job.sh —— 适配 cron 的脚本模板
set -euo pipefail

# cron 环境变量很少,需要显式设置
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export HOME="$(getent passwd "$(whoami)" | cut -d: -f6)"

readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_DIR="/var/log/cron"
readonly LOCK_DIR="/tmp/cron_locks"

mkdir -p "$LOG_DIR" "$LOCK_DIR"

# 锁文件防重复执行
LOCK_FILE="$LOCK_DIR/${SCRIPT_NAME}.lock"

cleanup() {
    rm -f "$LOCK_FILE"
}
trap cleanup EXIT

# 检查是否已在运行
if [[ -f "$LOCK_FILE" ]]; then
    old_pid=$(cat "$LOCK_FILE")
    if kill -0 "$old_pid" 2>/dev/null; then
        echo "脚本已在运行 (PID: $old_pid),退出" >> "$LOG_DIR/${SCRIPT_NAME}.log"
        exit 0
    fi
fi

echo $$ > "$LOCK_FILE"

# 日志输出(cron 不显示终端)
exec > >(tee -a "$LOG_DIR/${SCRIPT_NAME}.log") 2>&1

echo "=== 任务开始: $(date) ==="

# 业务逻辑
your_main_function

echo "=== 任务结束: $(date) ==="

安装 cron 任务

#!/bin/bash
# install_cron.sh —— 安装 cron 任务
set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly CRON_ENTRIES=(
    "0 2 * * * $SCRIPT_DIR/backup.sh >> /var/log/cron/backup.log 2>&1"
    "*/5 * * * * $SCRIPT_DIR/monitor.sh >> /var/log/cron/monitor.log 2>&1"
    "0 9 * * 1-5 $SCRIPT_DIR/report.sh >> /var/log/cron/report.log 2>&1"
)

# 备份现有 crontab
crontab -l > /tmp/crontab_backup_$(date +%Y%m%d) 2>/dev/null || true

# 添加 cron 任务(避免重复)
for entry in "${CRON_ENTRIES[@]}"; do
    if crontab -l 2>/dev/null | grep -qF "${entry%% *}"; then
        echo "⚠️  任务已存在: ${entry%% *}"
    else
        (crontab -l 2>/dev/null; echo "$entry") | crontab -
        echo "✅ 已添加: $entry"
    fi
done

echo ""
echo "当前 cron 任务:"
crontab -l

16.7 日志管理

# 日志轮转配置
cat > /etc/logrotate.d/myapp << 'EOF'
/var/log/myapp/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0640 myapp myapp
    sharedscripts
    postrotate
        systemctl reload myapp > /dev/null 2>&1 || true
    endscript
}
EOF

# 手动触发日志轮转
logrotate -f /etc/logrotate.d/myapp

# 查看系统日志
journalctl -xe                           # 最近日志
journalctl -u nginx --since "1 hour ago" # 指定服务
journalctl -p err                        # 仅错误级别
journalctl --disk-usage                   # 日志占用空间
journalctl --vacuum-size=1G              # 清理到指定大小

16.8 注意事项

陷阱说明解决方案
cron 环境变量缺失PATH 很短脚本中显式设置 PATH
cron 日志不可见没有终端重定向到文件
重复执行cron 可能重叠执行使用锁文件
root 权限滥用脚本以 root 运行使用最小权限原则
备份空间不足备份文件不断增长设置保留策略

16.9 扩展阅读