UNIX系统复习-4

这是我复习《UNIX系统》这门课程时记录的一些笔记 ,希望能对你有所帮助😊

Shell编程

一、Shell编程的意义

  1. 将命令组合成实用工具

    • 示例:创建显示目录的脚本
      1
      2
      3
      4
      5
      6
      # lsdir脚本内容
      ls -l | sed -n '/^d/p'

      # 使用方式
      chmod +x lsdir
      ./lsdir
  2. 快速编写实用软件

    • 示例:带行号的文件查看器
      1
      2
      3
      4
      5
      6
      # mycat脚本内容
      awk '{print NR, ": ", $0}' $1

      # 使用方式
      chmod +x mycat
      ./mycat filename

二、位置参数

  1. 基本位置参数

    • $0:脚本名称
    • $1, $2, …:第1、2个参数等
    • $#:参数个数
    • $*:所有参数集合
  2. 参数重置与移动

    • 使用set重置参数:
      1
      set new1 new2  # 重置$1为new1,$2为new2
    • 使用shift移动参数:
      1
      2
      shift    # 左移1位,丢弃$1
      shift 2 # 左移2位

三、Shell命令行结构

  1. 命令组合

    • 顺序执行:cmd1; cmd2
    • 管道:cmd1 | cmd2
    • 后台执行:cmd &
  2. 命令分组

    • 子shell执行:(cmd1; cmd2)
    • 当前shell执行:{ cmd1; cmd2; }

四、命令行模式与元字符

元字符功能描述
>输出重定向
>>追加输出
<输入重定向
|管道
*通配符
?单字符匹配
[a-z]字符范围匹配
;命令分隔符
&后台执行
`命令替换
$()命令替换(推荐)
#注释

五、引号使用

  1. 单引号:完全原样输出

    1
    echo '$USER `date`'  # 输出: $USER `date`
  2. 双引号:允许变量和命令替换

    1
    echo "$USER `date`"  # 输出当前用户和日期
  3. 反引号:命令替换(已过时,推荐使用$())

    1
    2
    echo "Today is `date`"
    echo "Today is $(date)" # 推荐写法

六、重定向

  1. 标准重定向

    1
    2
    3
    cmd > file    # 输出重定向
    cmd >> file # 追加输出
    cmd < file # 输入重定向
  2. 错误重定向

    1
    2
    3
    cmd 2> error.log          # 错误输出到文件
    cmd > output.log 2>&1 # 标准和错误都输出到文件
    cmd &> all_output.log # 同上(更简洁)
  3. Here Document

    1
    2
    3
    4
    cat <<EOF
    多行文本
    可以直接输入
    EOF

七、Shell程序结构

  1. 基本结构:命令序列
  2. 主要命令类型
    • 普通命令
    • 赋值命令
    • 运算命令
    • 流程控制(if/case/for/while)

八、流程控制

  1. if语句

    1
    2
    3
    4
    5
    6
    7
    if [ condition ]; then
    commands
    elif [ condition ]; then
    commands
    else
    commands
    fi
  2. case语句

    1
    2
    3
    4
    5
    case $var in
    pattern1) commands ;;
    pattern2) commands ;;
    *) default commands ;;
    esac
  3. for循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 形式1
    for var in list; do
    commands
    done

    # 形式2(C风格)
    for ((i=0; i<10; i++)); do
    commands
    done
  4. while循环

    1
    2
    3
    while [ condition ]; do
    commands
    done
  5. until循环

    1
    2
    3
    until [ condition ]; do
    commands
    done

九、test命令与条件判断

test

  1. 文件测试

    1
    2
    3
    4
    [ -e file ]    # 文件存在
    [ -f file ] # 是普通文件
    [ -d file ] # 是目录
    [ -s file ] # 文件非空
  2. 字符串测试

    1
    2
    3
    [ -z "$str" ]  # 字符串为空
    [ -n "$str" ] # 字符串非空
    [ "$a" = "$b" ] # 字符串相等
  3. 数值比较

    1
    2
    3
    4
    [ $a -eq $b ]  # 等于
    [ $a -ne $b ] # 不等于
    [ $a -gt $b ] # 大于
    [ $a -lt $b ] # 小于

十、Shell内部变量

变量描述
$?上条命令退出状态
$$当前shell的PID
$!最后后台进程PID
$#参数个数
$@所有参数(保留引号)
$*所有参数(不保留引号)
$_上条命令的最后一个参数

十一、实用示例

  1. 查找命令路径(mywhich)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/bin/bash
    case $# in
    0) echo "Usage: $0 cmd"; exit 1 ;;
    esac

    IFS=:
    for dir in $PATH; do
    if [ -x "$dir/$1" ]; then
    echo "$dir/$1"
    exit 0
    fi
    done
    echo "$1: not found"
    exit 1
  2. 倒序显示参数

    1
    2
    3
    4
    #!/bin/bash
    for ((i=$#; i>0; i--)); do
    echo ${!i} # 间接引用
    done

十二、Shell编程优缺点

优点

  • 快速组合现有命令
  • 无需编译,修改方便
  • 适合系统管理任务

缺点

  • 执行效率较低
  • 复杂逻辑可读性差
  • 可移植性受限

最佳实践建议

  1. 总在第一行指定解释器:#!/bin/bash
  2. 使用$()代替反引号进行命令替换
  3. 变量引用加双引号:"$var"
  4. 使用[[ ]]代替[ ]进行条件测试(更强大)
  5. 为脚本添加帮助信息和错误处理
  6. 复杂脚本考虑使用Python等更强大的语言

通过掌握这些核心知识,您可以编写出高效、可靠的Shell脚本,完成各种系统管理和自动化任务。

C语言开发环境

一、概述

  1. C语言与UNIX的关系

    • 1973年由Dennis Ritchie发明,用于重写UNIX系统
    • 成为UNIX系统的”自然”语言
    • 1988年IEEE推出POSIX标准,统一了C语言头文件
  2. C语言优势

    • 高效性:直接操作硬件
    • 可移植性:POSIX标准确保跨平台兼容性
    • 广泛应用:系统编程、嵌入式开发首选语言

二、C程序实例分析

1. 基本结构

1
2
3
4
5
6
7
/* 注释:传统第一个C程序 */
#include <stdio.h> // 预编译指令:包含标准I/O头文件

int main(int argc, char *argv[]) { // 主函数,程序入口
printf("Hello World!\n"); // 输出语句
return 0; // 返回值(0表示成功)
}

2. 关键语法解析

  1. 注释

    • /* ... */:多行注释
    • //:单行注释(C99标准)
  2. 预编译指令

    • #include <头文件>:从系统路径(/usr/include)加载
    • #include "头文件":从当前目录加载
  3. 主函数

    • 必须的入口函数
    • 参数:
      • argc:参数个数
      • argv[]:参数数组(argv[0]为程序名)
    • 返回值:
      • 0表示成功
      • 非0表示错误(便于Shell脚本判断)

三、C编译过程

1. 四个阶段

  1. 预处理

    • 处理#include#define等指令
    • 生成.i文件
  2. 编译

    • 词法/语法分析
    • 生成汇编代码(.s文件)
  3. 汇编

    • 将汇编代码转为机器码
    • 生成目标文件(.o)
  4. 链接

    • 合并多个目标文件
    • 生成可执行程序

2. 常用GCC命令

命令功能
gcc hello.c生成a.out
gcc -o hello hello.c指定输出文件名
gcc -c file.c仅编译生成.o文件
gcc -g program.c加入调试信息

四、调试工具GDB

1. 核心功能

  • 设置断点
  • 单步执行
  • 查看变量值
  • 修改程序状态

2. 常用命令

命令功能
gdb ./a.out启动调试
break main在main函数设断点
run运行程序
print x查看变量x的值
next单步执行(不进入函数)
quit退出GDB

五、工程管理工具Make

1. Makefile规则

1
2
3
目标文件: 依赖文件1 依赖文件2 ...
<tab>命令1
<tab>命令2

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
app: main.o utils.o
gcc -o app main.o utils.o

main.o: main.c
gcc -c main.c

utils.o: utils.c
gcc -c utils.c

clean:
rm -f *.o app
.PHONY: clean

3. 重要概念

  1. 变量

    1
    2
    3
    OBJS = main.o utils.o
    app: $(OBJS)
    gcc -o app $(OBJS)
  2. 隐含规则

    • 自动推导依赖关系
    • .o文件自动从.c文件生成
  3. 伪目标

    • 用于非编译任务(如clean)
    • 需用.PHONY声明

六、标准C函数

1. 系统调用 vs 库函数

特性系统调用库函数
提供者操作系统内核编程语言
功能基础接口高级封装
示例write()printf()
可替换性不可替换可替换

2. UNIX体系结构

1
应用程序 → 库函数 → 系统调用 → 内核

3. 标准化

  1. ISO C

    • C90(1989)、C99(1999)
    • 定义24个标准头文件
  2. POSIX

    • 增强UNIX系统可移植性
    • 定义26个必需头文件
  3. SUS

    • 单一UNIX规范
    • 扩展POSIX功能

系统调用

一、文件I/O(文件操作)

1. 文件描述符(File Descriptor, fd)

  • 定义:非负整数,用于标识进程打开的文件。
  • 默认文件描述符
    • 0:标准输入(STdin_FILENO,键盘输入)。
    • 1:标准输出(STDOUT_FILENO,屏幕输出)。
    • 2:标准错误(STDERR_FILENO,屏幕错误输出)。

示例:读取标准输入并写入标准输出

1
2
3
4
5
6
7
8
9
#include <unistd.h>
#include <stdio.h>

int main() {
char buf[1024];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); // 从键盘读取
write(STDOUT_FILENO, buf, n); // 输出到屏幕
return 0;
}

解释

  • read(fd, buf, size):从 fd 读取最多 size 字节到 buf,返回实际读取字节数。
  • write(fd, buf, size):将 bufsize 字节写入 fd

2. open()creat()

open() 函数

1
2
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
  • **flags**:
    • O_RDONLY:只读。
    • O_WRONLY:只写。
    • O_RDWR:读写。
    • O_CREAT:文件不存在时创建。
    • O_TRUNC:若文件存在,清空内容。
    • O_APPEND:追加写入(避免并发问题)。

示例:打开/创建文件

1
2
3
4
5
6
7
8
9
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Hello, World!\n", 14);
close(fd);
return 0;
}

解释

  • O_CREAT | O_TRUNC:若文件不存在则创建,若存在则清空。
  • 0644:文件权限(rw-r--r--)。

3. lseek():移动文件指针

1
2
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • **whence**:
    • SEEK_SET:从文件开头计算偏移。
    • SEEK_CUR:从当前位置计算偏移。
    • SEEK_END:从文件末尾计算偏移。

示例:读取文件第 10 字节

1
2
3
4
5
6
7
8
9
10
11
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("test.txt", O_RDONLY);
lseek(fd, 10, SEEK_SET); // 移动到第 10 字节
char buf[1];
read(fd, buf, 1); // 读取 1 字节
close(fd);
return 0;
}

4. dup2():文件描述符重定向

1
2
#include <unistd.h>
int dup2(int oldfd, int newfd);
  • 作用:让 newfd 指向 oldfd 的文件。

示例:将标准输出重定向到文件

1
2
3
4
5
6
7
8
9
10
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // 标准输出指向文件
printf("This goes to output.txt!\n"); // 写入文件而非屏幕
close(fd);
return 0;
}

文件属性及目录

一、文件属性(struct stat

1. 属性结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/stat.h>
struct stat {
mode_t st_mode; // 文件类型和权限(核心字段)
ino_t st_ino; // inode 号(唯一标识文件)
dev_t st_dev; // 文件所在设备的 ID
dev_t st_rdev; // 设备文件的设备号(仅特殊文件有效)
nlink_t st_nlink; // 硬链接计数
uid_t st_uid; // 所有者用户 ID
gid_t st_gid; // 所有者组 ID
off_t st_size; // 文件大小(字节)
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改内容时间
time_t st_ctime; // 最后修改属性时间(如权限)
};

2. 获取文件属性

  • **stat()**:通过路径获取属性。
    1
    int stat(const char *pathname, struct stat *buf);
  • **fstat()**:通过文件描述符获取属性。
    1
    int fstat(int fd, struct stat *buf);

示例:打印文件大小

1
2
3
4
5
6
7
8
9
#include <sys/stat.h>
#include <stdio.h>

int main() {
struct stat buf;
stat("test.txt", &buf);
printf("File size: %ld bytes\n", buf.st_size);
return 0;
}

二、文件类型与权限

1. 文件类型判断宏

文件类型
S_ISREG(mode)普通文件(如 .txt
S_ISDIR(mode)目录
S_ISCHR(mode)字符设备(如 /dev/tty
S_ISBLK(mode)块设备(如 /dev/sda
S_ISFIFO(mode)管道(FIFO)
S_ISLNK(mode)符号链接
S_ISSOCK(mode)套接字

示例:判断文件类型

1
2
3
if (S_ISREG(buf.st_mode)) {
printf("This is a regular file.\n");
}

2. 文件权限

  • 9 个权限位
    • 用户S_IRUSR(读)、S_IWUSR(写)、S_IXUSR(执行)。
    • S_IRGRPS_IWGRPS_IXGRP
    • 其他S_IROTHS_IWOTHS_IXOTH
  • 特殊权限位
    • S_ISUID:执行时设置有效用户 ID(如 passwd)。
    • S_ISGID:执行时设置有效组 ID。
    • S_ISVTX:粘滞位(目录下文件仅所有者可删)。

修改权限

1
2
chmod("file.txt", 0644);  // rw-r--r--
fchmod(fd, 0755); // rwxr-xr-x

三、文件操作

1. 创建新文件

  • 权限计算实际权限 = mode & ~umask
    umask 默认为 022,屏蔽组和其他的写权限)
  • 示例
    1
    2
    umask(002);                    // 设置 umask
    int fd = open("new.txt", O_CREAT | O_WRONLY, 0666); // 实际权限:664

2. 硬链接与删除

  • **link()**:创建硬链接(增加 st_nlink)。
    1
    link("old.txt", "new.txt");  // new.txt 指向 old.txt
  • **unlink()**:删除链接(st_nlink--,若为 0 则删除文件)。
    1
    unlink("file.txt");  // 删除文件(需无进程打开)
  • **remove()**:等价于 unlink()(文件)或 rmdir()(目录)。

3. 重命名

1
rename("old.txt", "new.txt");  // 原子操作

四、目录操作

1. 创建与删除目录

1
2
mkdir("mydir", 0755);  // 创建目录
rmdir("mydir"); // 删除空目录

2. 读取目录内容

1
2
3
4
5
#include <dirent.h>
DIR *dp = opendir("."); // 打开目录
struct dirent *dir = readdir(dp); // 读取条目
printf("File: %s\n", dir->d_name); // 打印文件名
closedir(dp); // 关闭目录

struct dirent 关键字段

  • d_name:文件名。
  • d_type:文件类型(如 DT_REG 普通文件)。

五、工作目录管理

1. 改变工作目录

1
2
chdir("/tmp");       // 切换到 /tmp
fchdir(dir_fd); // 通过文件描述符切换

2. 获取当前目录

1
2
3
char cwd[1024];
getcwd(cwd, sizeof(cwd)); // 类似 pwd 命令
printf("CWD: %s\n", cwd);

六、总结表

功能函数/宏说明
获取属性stat(), fstat()填充 struct stat
文件类型判断S_ISREG(), S_ISDIR()检查 st_mode
修改权限chmod(), fchmod()设置 rwx 权限
创建文件open() with O_CREATumask 影响
硬链接操作link(), unlink()修改 st_nlink
目录操作mkdir(), opendir()创建/遍历目录
工作目录chdir(), getcwd()类似 cdpwd

关键点

  1. st_mode 是核心字段:包含文件类型和权限。
  2. 权限计算实际权限 = mode & ~umask
  3. 硬链接 vs 符号链接
    • 硬链接:同一文件,共享 inode。
    • 符号链接:独立文件,存储目标路径。
  4. 目录操作:只有内核可写目录,用户通过 opendir/readdir 读取。

建议:结合 ls -l 命令和 stat 命令验证文件属性。

二、进程控制

1. fork():创建子进程

1
2
#include <unistd.h>
pid_t fork(void);
  • 返回值
    • >0:父进程,返回子进程 PID。
    • =0:子进程。
    • -1:出错。

示例:父子进程执行不同任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>

int main() {
pid_t pid = fork();
if (pid == 0) {
printf("Child process (PID=%d)\n", getpid());
} else if (pid > 0) {
printf("Parent process (PID=%d), child PID=%d\n", getpid(), pid);
} else {
perror("fork failed");
}
return 0;
}

输出

1
2
Parent process (PID=1234), child PID=1235
Child process (PID=1235)

2. exec():替换进程映像

1
2
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., (char *)0);
  • 作用:用新程序替换当前进程(如 lsgcc)。

示例:子进程执行 ls -l

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
#include <stdio.h>

int main() {
pid_t pid = fork();
if (pid == 0) {
execl("/bin/ls", "ls", "-l", NULL); // 替换为 ls -l
perror("exec failed"); // 若 exec 失败才会执行
} else {
wait(NULL); // 等待子进程结束
}
return 0;
}

exec 函数族对比表

以下是 UNIX/Linux 中 exec 系列函数的详细对比,包括参数、使用场景和区别:

函数参数形式环境变量查找路径典型用途
execlexecl("路径", "arg0", "arg1", ..., (char *)0)继承当前需完整路径已知可执行文件路径,参数列表固定
execvexecv("路径", char *argv[])argv 需以 NULL 结尾)继承当前需完整路径参数列表动态(如数组)
execleexecle("路径", "arg0", ..., (char *)0, char *envp[])自定义需完整路径需指定环境变量
execveexecve("路径", char *argv[], char *envp[])argvenvp 需以 NULL 结尾)自定义需完整路径系统调用,最底层实现
execlpexeclp("文件名", "arg0", ..., (char *)0)继承当前PATH 查找直接使用文件名(如 ls
execvpexecvp("文件名", char *argv[])argv 需以 NULL 结尾)继承当前PATH 查找动态参数 + PATH 查找

关键区别

  1. 参数传递方式

    • l 后缀(如 execl):参数以可变参数列表arg0, arg1, ..., NULL)传递。
    • v 后缀(如 execv):参数以指针数组argv[])传递,数组必须以 NULL 结尾。
  2. 环境变量

    • e 后缀(如 execle):可自定义环境变量(通过 envp[] 数组)。
    • e 后缀:继承当前进程的环境变量。
  3. 路径查找

    • p 后缀(如 execlp):自动从 PATH 环境变量查找可执行文件。
    • p 后缀:必须提供完整路径(如 /bin/ls)。
  4. 底层实现

    • execve 是唯一的系统调用,其他函数均为库函数(最终调用 execve)。

示例代码

1. execl(固定参数 + 完整路径)

1
execl("/bin/ls", "ls", "-l", NULL);  // 执行 `ls -l`

2. execv(动态参数 + 完整路径)

1
2
char *args[] = {"ls", "-l", NULL};
execv("/bin/ls", args);

3. execle(自定义环境变量)

1
2
char *env[] = {"PATH=/usr/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, env);

4. execlp(自动查找 PATH

1
execlp("ls", "ls", "-l", NULL);  // 无需写 `/bin/ls`

5. execvp(动态参数 + 自动查找)

1
2
char *args[] = {"ls", "-l", NULL};
execvp("ls", args);

总结

  • 需要完整路径 → 用 execlexecvexecleexecve
  • 需要 PATH 查找 → 用 execlpexecvp
  • 动态参数 → 用 execvexecvpexecve
  • 自定义环境变量 → 用 execleexecve

注意:所有 exec 函数成功时不返回,失败时返回 -1 并设置 errno


3. wait():回收子进程

1
2
#include <sys/wait.h>
pid_t wait(int *status);
  • 作用:父进程等待子进程结束,避免僵尸进程。

示例:父进程等待子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
pid_t pid = fork();
if (pid == 0) {
printf("Child process\n");
sleep(2);
} else {
wait(NULL); // 等待子进程结束
printf("Parent process: child finished\n");
}
return 0;
}

三、进程间通信(IPC)

1. 管道(pipe

1
2
#include <unistd.h>
int pipe(int fd[2]);
  • **fd[0]**:读端。
  • **fd[1]**:写端。

示例:父子进程通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>

int main() {
int fd[2];
pipe(fd); // 创建管道

pid_t pid = fork();
if (pid == 0) {
close(fd[0]); // 子进程关闭读端
write(fd[1], "Hello, parent!", 14);
} else {
close(fd[1]); // 父进程关闭写端
char buf[20];
read(fd[0], buf, sizeof(buf));
printf("Parent received: %s\n", buf);
}
return 0;
}

输出

1
Parent received: Hello, parent!
  • 再给一个例子,好好理解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <unistd.h>
#include <stdio.h>

int fd[2];
int main() {
pipe(fd);
printf("%d %d\n", fd[0], fd[1]);
pid_t pid = fork();
if (pid == 0) {
close(fd[1]);
dup2(fd[0], 0);
execlp("wc", "wc", "-l", (char *)0);
printf("hello world!\n");
} else {
close(fd[0]);
printf("before dup2\n");
dup2(fd[1], 1);
printf("after dup2\n");
}
printf("lalalala this line shouldn't be print\n");
}
1
2
3
3  4
before dup2
2

四、信号(Signal)

1. signal():注册信号处理函数

1
2
#include <signal.h>
void (*signal(int sig, void (*handler)(int)))(int);
  • 常见信号
    • SIGINTCtrl+C)。
    • SIGKILL(强制终止)。
    • SIGALRM(定时器)。

示例:捕获 Ctrl+C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handler(int sig) {
printf("\nReceived SIGINT (Ctrl+C)\n");
_exit(1); // 退出程序
}

int main() {
signal(SIGINT, handler); // 注册信号处理函数
while (1) {
printf("Running...\n");
sleep(1);
}
return 0;
}

运行

1
2
3
4
Running...
Running...
^C
Received SIGINT (Ctrl+C)

总结

主题关键函数用途
文件I/Oopen, read, write, lseek, dup2文件操作、重定向
进程控制fork, exec, wait创建进程、执行程序
IPCpipe, mkfifo进程间通信
信号signal, kill, alarm异步事件处理

建议

  • 结合代码示例动手实验,加深理解。
  • 使用 strace 观察系统调用执行过程(如 strace ./a.out)。

UNIX系统复习-4
http://pzhwuhu.github.io/2025/05/31/UNIX系统复习-4/
本文作者
pzhwuhu
发布于
2025年5月31日
更新于
2025年5月31日
许可协议