banner
NEWS LETTER

Linux 系统编程入门:文件 IO、进程、线程与信号

Scroll down

一、系统编程学什么

Linux 系统编程关注应用程序和操作系统之间的接口。普通业务代码更多关心业务逻辑,系统编程则关心程序如何使用文件、进程、线程、信号、内存和网络。

入门阶段重点掌握:

  • 文件描述符。
  • openreadwriteclose
  • 进程创建和等待。
  • 线程创建和同步。
  • 信号处理。
  • 错误码和 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
2
./file_io
cat hello.txt

注意点:

  • 系统调用失败通常返回 -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:资源暂时不可用。

系统编程的代码通常比普通业务代码更重视错误分支,因为很多问题只会在真实环境中出现。

十、学习建议

建议按这个顺序练习:

  1. 文件读写。
  2. 目录遍历。
  3. 子进程创建和等待。
  4. 执行外部命令。
  5. 多线程计数器。
  6. 信号控制程序退出。
  7. strace 观察自己的程序。

系统编程最重要的是理解程序和操作系统之间的边界:你的代码不是孤立运行的,它一直在向内核申请资源。

其他文章
目录导航 置顶
  1. 1. 一、系统编程学什么
  2. 2. 二、文件描述符
  3. 3. 三、基础文件 IO
  4. 4. 四、进程
  5. 5. 五、执行新程序
  6. 6. 六、线程
  7. 7. 七、互斥锁
  8. 8. 八、信号
  9. 9. 九、错误处理
  10. 10. 十、学习建议
请输入关键词进行搜索