在Linux环境下进行系统编程的第三天,我开始真正理解操作系统底层的工作机制。不同于前两天的环境搭建和基础命令学习,第三天的内容开始涉及进程控制、文件描述符和系统调用等核心概念。这些知识对于想要深入理解Linux系统工作原理的开发者来说至关重要。
我记得第一次用fork()创建子进程时那种既兴奋又困惑的感觉——明明是一个程序,怎么就突然变成了两个?还有管道通信的实现,让我第一次体会到进程间通信的神奇。这些概念在Windows编程中很少直接接触,但在Linux环境下却是家常便饭。
fork()是Linux系统编程中最基础也最重要的系统调用之一。它的功能简单来说就是复制当前进程,创建一个几乎完全相同的子进程。这个"几乎"很关键,因为子进程会继承父进程的代码段、数据段、堆栈和打开的文件描述符等资源,但也有几点不同:
c复制#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
printf("这是子进程,PID=%d\n", getpid());
} else {
printf("这是父进程,子进程PID=%d\n", pid);
}
return 0;
}
注意:fork()调用一次会返回两次——在父进程中返回子进程的PID,在子进程中返回0。这是判断当前代码是在父进程还是子进程中执行的依据。
fork()创建的子进程默认会执行与父进程相同的代码,如果想让子进程执行不同的程序,就需要使用exec族函数。exec系列函数会用新程序替换当前进程的代码段、数据段等,但保留进程ID和打开的文件描述符等属性。
常见的exec函数包括:
c复制#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程执行ls命令
execlp("ls", "ls", "-l", NULL);
perror("execlp failed");
return 1;
} else if (pid > 0) {
printf("父进程继续执行\n");
}
return 0;
}
管道是Linux进程间通信(IPC)最基本的方式之一,分为无名管道和有名管道。无名管道只能用于有亲缘关系的进程间通信,通常就是父子进程。
创建管道的系统调用是pipe(),它会返回两个文件描述符:pipefd[0]用于读,pipefd[1]用于写。
c复制#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int pipefd[2];
char buf[256];
if (pipe(pipefd) == -1) {
perror("pipe failed");
return 1;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:关闭写端,从管道读取数据
close(pipefd[1]);
read(pipefd[0], buf, sizeof(buf));
printf("子进程收到: %s\n", buf);
close(pipefd[0]);
} else {
// 父进程:关闭读端,向管道写入数据
close(pipefd[0]);
const char* msg = "Hello from parent";
write(pipefd[1], msg, strlen(msg)+1);
close(pipefd[1]);
}
return 0;
}
提示:管道是半双工的,数据只能单向流动。如果需要双向通信,应该创建两个管道。
Linux中一切皆文件,文件描述符是访问这些"文件"的句柄。dup()和dup2()系统调用可以复制文件描述符,常用于实现I/O重定向。
c复制#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open failed");
return 1;
}
// 将标准输出重定向到文件
dup2(fd, STDOUT_FILENO);
close(fd);
printf("这行文字会被写入output.txt文件\n");
return 0;
}
信号是Linux系统中进程间通信的另一种基本方式,用于通知进程发生了某种事件。常见的信号包括:
可以使用signal()或更强大的sigaction()来设置信号处理函数。
c复制#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig) {
printf("收到信号 %d\n", sig);
}
int main() {
signal(SIGINT, handler);
printf("按Ctrl+C测试信号处理\n");
pause(); // 等待信号
return 0;
}
Linux系统中,进程除了有自己的PID外,还属于某个进程组(PGID)和会话(SID)。理解这些概念对于实现shell作业控制等功能很重要。
c复制#include <unistd.h>
#include <stdio.h>
int main() {
printf("当前进程PID=%d, PGID=%d, SID=%d\n",
getpid(), getpgid(0), getsid(0));
// 创建新会话
if (setsid() == -1) {
perror("setsid failed");
return 1;
}
printf("新会话SID=%d\n", getsid(0));
return 0;
}
当子进程终止但父进程没有调用wait()或waitpid()来获取其终止状态时,子进程就会变成僵尸进程。僵尸进程不占用内存等资源,但会占用进程表中的位置。
避免僵尸进程的几种方法:
c复制#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("子进程运行\n");
return 0;
} else {
// 方法1:等待子进程
wait(NULL);
// 方法2:忽略SIGCHLD
// signal(SIGCHLD, SIG_IGN);
printf("父进程继续\n");
}
return 0;
}
调试多进程程序比单进程复杂,以下是一些实用技巧:
使用gdb的follow-fork-mode选项控制调试哪个进程
bash复制gdb -ex "set follow-fork-mode child" ./program
在代码中插入调试输出,打印进程ID和关键变量值
使用ps命令查看进程状态
bash复制ps -eo pid,ppid,pgid,sid,comm | grep program
使用strace跟踪系统调用
bash复制strace -f ./program
为了巩固第三天的学习内容,我尝试实现了一个简单的shell,支持基本命令执行和管道功能。这个练习帮助我更好地理解了Linux shell的工作原理。
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_ARGS 10
#define MAX_CMD_LEN 100
void parse_command(char* cmd, char** args) {
int i = 0;
char* token = strtok(cmd, " ");
while (token != NULL && i < MAX_ARGS-1) {
args[i++] = token;
token = strtok(NULL, " ");
}
args[i] = NULL;
}
void execute_command(char** args) {
pid_t pid = fork();
if (pid == 0) {
execvp(args[0], args);
perror("execvp failed");
exit(1);
} else if (pid > 0) {
wait(NULL);
} else {
perror("fork failed");
}
}
int main() {
char cmd[MAX_CMD_LEN];
char* args[MAX_ARGS];
while (1) {
printf("mysh> ");
fgets(cmd, MAX_CMD_LEN, stdin);
cmd[strcspn(cmd, "\n")] = '\0'; // 去除换行符
if (strcmp(cmd, "exit") == 0) {
break;
}
parse_command(cmd, args);
execute_command(args);
}
return 0;
}
这个简单的shell可以解析并执行基本命令,如"ls -l"等。要实现管道功能,还需要扩展代码来处理"|"符号并创建管道连接两个命令。