一、系统编程学什么
Linux 系统编程关注应用程序和操作系统之间的接口。普通业务代码更多关心业务逻辑,系统编程则关心程序如何使用文件、进程、线程、信号、内存和网络。
入门阶段重点掌握:
- 文件描述符。
open、read、write、close。
- 进程创建和等待。
- 线程创建和同步。
- 信号处理。
- 错误码和
errno。
二、文件描述符
Linux 中很多资源都可以看成文件:
- 普通文件。
- 终端。
- socket。
- pipe。
- 设备节点。
程序通过文件描述符访问这些资源。文件描述符是一个整数。
默认有三个:
1 2 3
| 0 stdin 1 stdout 2 stderr
|
三、基础文件 IO
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <fcntl.h> #include <stdio.h> #include <unistd.h>
int main(void) { int fd = open("hello.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd < 0) { perror("open"); return 1; }
const char *msg = "hello linux io\n"; ssize_t n = write(fd, msg, 15); if (n < 0) { perror("write"); close(fd); return 1; }
close(fd); return 0; }
|
编译:
1
| gcc file_io.c -o file_io -Wall -Wextra -g
|
运行:
注意点:
- 系统调用失败通常返回
-1。
perror 可以打印最近一次错误原因。
- 打开的文件最后要
close。
四、进程
进程是程序的一次运行实例。Linux 中常用 fork 创建子进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <stdio.h> #include <sys/wait.h> #include <unistd.h>
int main(void) { pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) { printf("child pid=%d\n", getpid()); return 0; }
printf("parent pid=%d child=%d\n", getpid(), pid); wait(NULL); return 0; }
|
fork 返回值:
- 小于 0:创建失败。
- 等于 0:当前在子进程。
- 大于 0:当前在父进程,返回值是子进程 pid。
五、执行新程序
fork 只复制当前进程。如果要运行另一个程序,通常配合 exec。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h> #include <sys/wait.h> #include <unistd.h>
int main(void) { pid_t pid = fork(); if (pid == 0) { execlp("ls", "ls", "-la", NULL); perror("execlp"); return 1; }
wait(NULL); return 0; }
|
如果 execlp 成功,后面的代码不会继续执行,因为当前进程映像已经被新程序替换。
六、线程
线程适合在同一个进程中并发执行任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <pthread.h> #include <stdio.h>
static void *worker(void *arg) { const char *name = arg; printf("worker: %s\n", name); return NULL; }
int main(void) { pthread_t tid;
if (pthread_create(&tid, NULL, worker, "task1") != 0) { perror("pthread_create"); return 1; }
pthread_join(tid, NULL); return 0; }
|
编译时需要链接 pthread:
1
| gcc thread_demo.c -o thread_demo -Wall -Wextra -g -pthread
|
线程之间共享进程地址空间,所以访问共享数据时要注意锁。
七、互斥锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <pthread.h> #include <stdio.h>
static int counter = 0; static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static void *worker(void *arg) { (void)arg; for (int i = 0; i < 100000; i++) { pthread_mutex_lock(&lock); counter++; pthread_mutex_unlock(&lock); } return NULL; }
int main(void) { pthread_t t1, t2; pthread_create(&t1, NULL, worker, NULL); pthread_create(&t2, NULL, worker, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("counter=%d\n", counter); return 0; }
|
没有锁时,多个线程同时修改 counter 会产生竞态条件。
八、信号
信号是 Linux 通知进程事件的一种方式。
常见信号:
SIGINT:Ctrl+C。
SIGTERM:请求退出。
SIGKILL:强制退出,不能捕获。
SIGHUP:终端断开或重新加载配置。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <signal.h> #include <stdio.h> #include <unistd.h>
static volatile sig_atomic_t running = 1;
static void on_signal(int signo) { (void)signo; running = 0; }
int main(void) { signal(SIGINT, on_signal); signal(SIGTERM, on_signal);
while (running) { printf("running...\n"); sleep(1); }
printf("exit\n"); return 0; }
|
后台服务应该处理 SIGTERM,收到后释放资源、关闭文件、断开连接,然后退出。
九、错误处理
系统调用失败后要检查返回值,不要假设一定成功。
1 2 3 4 5
| int fd = open("app.conf", O_RDONLY); if (fd < 0) { perror("open app.conf"); return 1; }
|
常见错误:
ENOENT:文件不存在。
EACCES:权限不足。
EINTR:系统调用被信号中断。
EAGAIN:资源暂时不可用。
系统编程的代码通常比普通业务代码更重视错误分支,因为很多问题只会在真实环境中出现。
十、学习建议
建议按这个顺序练习:
- 文件读写。
- 目录遍历。
- 子进程创建和等待。
- 执行外部命令。
- 多线程计数器。
- 信号控制程序退出。
- 用
strace 观察自己的程序。
系统编程最重要的是理解程序和操作系统之间的边界:你的代码不是孤立运行的,它一直在向内核申请资源。