第九周预习报告
文章目录
- 第九周预习报告
- 学习内容
- AI 对学习内容的总结
- 章节概述
- 内容总结
- 对知识总结的反思
- 思维导图
- 基于AI的学习
- 知识点1:系统调用
- 知识点2:进程控制
- 学习实践过程遇到的问题与解决方式
- 问题1:`fork()` 调用后,子进程和父进程同时运行导致输出混乱
- 问题2:`exec()` 调用失败,程序无法执行新程序
- 问题3:`open()` 系统调用失败,无法打开文件
- 问题4:`wait()` 调用后,父进程无法正确捕获子进程的退出状态
- 问题5:环境变量传递失败,子进程无法获取正确的环境变量
- 参考资料
第九周预习报告
学习内容
- Head First C 第9章
AI 对学习内容的总结
章节概述
- 本章主要介绍进程和系统调用的概念,学习如何在自己的代码内部使用其他程序,学会如何使用系统服务来创建和控制进程。使程序能够访问电子邮件、Web以及其他任何安装的工具。
内容总结
-
系统调用:
- 定义:系统调用是用户程序与操作系统内核之间的接口,允许用户程序请求操作系统执行特定的任务。
- 常见系统调用:
open()
:打开文件。read()
:从文件中读取数据。write()
:将数据写入文件。close()
:关闭文件。fork()
:创建一个新进程。exec()
:执行一个新程序。wait()
:等待子进程结束。exit()
:终止当前进程。
- 工作原理:当用户程序调用一个系统调用时,CPU会从用户模式切换到内核模式,内核执行相应的操作,完成后返回结果,CPU再切换回用户模式。
-
进程控制:
- 进程:进程是操作系统进行资源分配和调度的基本单位,每个进程都有自己的地址空间、文件描述符、环境变量等。
- 创建进程:
fork()
:创建一个新进程,新进程是调用进程的副本,两者几乎完全相同,但有一个唯一的区别:fork()
返回值不同。在父进程中返回子进程的PID,在子进程中返回0。
- 执行新程序:
exec()
:有一系列函数(如execl()
、execlp()
、execle()
、execv()
、execvp()
、execvpe()
),用于替换当前进程的地址空间,使其开始执行一个新程序。
- 等待子进程:
wait()
:父进程调用wait()
等待子进程结束,wait()
会阻塞父进程直到子进程结束。
- 终止进程:
exit()
:终止当前进程,可以传递一个退出状态码给父进程。
-
环境变量传递:
- 定义:环境变量是操作系统提供的一种机制,用于在进程之间传递配置信息。
- 传递方式:子进程会继承父进程的环境变量,可以通过
exec()
系列函数传递新的环境变量。
-
错误处理:
- 返回值:大多数系统调用都有一个返回值,成功时返回一个有意义的值,失败时返回一个错误码(通常是负数)。
- errno:全局变量
errno
用于存储系统调用的错误代码,可以通过strerror()
函数将错误代码转换为人类可读的字符串。#include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { int fd = open("nonexistent_file.txt", O_RDONLY); if (fd == -1) { fprintf(stderr, "Error opening file: %s\n", strerror(errno)); return 1; } close(fd); return 0; }
-
RSS订阅:
- 定义:RSS(Really Simple Syndication)是一种用于发布经常更新的内容的标准格式,如博客文章、新闻等。
- 实现:通过系统调用读取RSS源,解析XML数据,提取所需信息。
#include <stdio.h> #include <curl/curl.h> size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) { size_t written = fwrite(ptr, size, nmemb, stream); return written; } int main() { CURL *curl; FILE *file; CURLcode res; curl_global_init(CURL_GLOBAL_DEFAULT); curl = curl_easy_init(); if(curl) { file = fopen("rss_feed.xml", "wb"); curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/rss.xml"); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); res = curl_easy_perform(curl); if(res != CURLE_OK) { fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); } fclose(file); curl_easy_cleanup(curl); } curl_global_cleanup(); return 0; }
-
父子进程关系:
- 父子关系:通过
fork()
创建的子进程是父进程的子进程,子进程可以调用getppid()
获取父进程的PID。#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "Fork failed\n"); return 1; } else if (pid == 0) { // 子进程 printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid()); execlp("/bin/ls", "ls", "-l", NULL); } else { // 父进程 printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid); wait(NULL); // 等待子进程结束 printf("Child process finished\n"); } return 0; }
- 父子关系:通过
对知识总结的反思
-
系统调用:
- 系统调用是用户程序与操作系统内核之间的桥梁,通过系统调用可以执行各种底层操作,如文件操作、进程控制等。
- 在实际编程中,需要仔细检查每个系统调用的返回值,以确保调用成功,并处理可能出现的错误。
-
进程控制:
- 进程控制是操作系统管理进程生命周期的关键部分,通过
fork()
和exec()
可以创建和执行新的进程。 - 在使用
fork()
和exec()
时,需要注意父子进程的关系,确保程序逻辑的正确性。
- 进程控制是操作系统管理进程生命周期的关键部分,通过
-
环境变量传递:
- 环境变量是进程之间传递配置信息的重要机制,子进程会继承父进程的环境变量。
- 在调用
exec()
系列函数时,可以通过参数传递新的环境变量。
-
错误处理:
- 错误处理是确保程序健壮性的关键,通过检查系统调用的返回值和使用
errno
,可以有效地处理错误。
- 错误处理是确保程序健壮性的关键,通过检查系统调用的返回值和使用
-
RSS订阅:
- RSS订阅是一个实际应用的例子,展示了如何使用系统调用和网络编程来获取和处理外部数据。
- 通过CURL库,可以方便地实现HTTP请求和数据下载。
思维导图
mindmap
本周学习内容
进程和系统调用
章节概述
系统调用
定义
常见系统调用
open
read
write
close
fork
exec
wait
exit
进程控制
进程
创建进程
fork
执行新程序
exec
等待子进程
wait
终止进程
exit
环境变量传递
定义
传递方式
错误处理
返回值
errno
RSS订阅
定义
实现
使用CURL库
解析XML数据
父子进程关系
父子关系
getppid
重点内容
系统调用
定义
常见系统调用
open
read
write
close
fork
exec
wait
exit
进程控制
进程
创建进程
fork
执行新程序
exec
等待子进程
wait
终止进程
exit
环境变量传递
定义
传递方式
错误处理
返回值
errno
RSS订阅
定义
实现
使用CURL库
解析XML数据
父子进程关系
父子关系
getppid
基于AI的学习
知识点1:系统调用
AI: 举一个常见的系统调用例子。
我: 打开文件是一个常见的系统调用。当程序想要读取或写入文件时,它会调用 open()
系统调用。这个调用会请求操作系统打开指定的文件,并返回一个文件描述符,程序可以使用这个文件描述符进行后续的读写操作。
AI: 系统调用 fork()
和 exec()
有什么区别?
我: fork()
和 exec()
是两个不同的系统调用,它们分别用于不同的目的。
fork()
用于创建一个新的进程。新进程是调用进程的副本,具有相同的代码段、数据段和堆栈段,但有自己的独立内存空间。fork()
返回一个值,如果返回值为0,则表示当前进程是子进程;如果返回值大于0,则表示当前进程是父进程,返回值是子进程的PID。exec()
用于在当前进程中加载并运行一个新的程序。exec()
有多个变种,如execl()
、execlp()
、execle()
、execv()
、execvp()
和execvpe()
,它们的主要区别在于参数的传递方式。调用exec()
之后,当前进程的代码段、数据段和堆栈段会被新程序的代码段、数据段和堆栈段替换。
AI: wait()
系统调用的作用是什么?
我: wait()
系统调用用于父进程等待子进程结束。父进程调用 wait()
会阻塞,直到一个子进程结束。wait()
返回子进程的PID,并且可以通过返回值和 wait()
的参数来获取子进程的退出状态。
AI: 如何处理系统调用的错误?
我: 处理系统调用的错误通常涉及以下几个步骤:
- 检查系统调用的返回值。大多数系统调用在成功时返回一个有意义的值,而在失败时返回一个错误码(通常是负数)。
- 如果系统调用失败,检查全局变量
errno
以获取具体的错误代码。 - 使用
strerror()
函数将错误代码转换为人类可读的错误信息,并进行适当的错误处理,如打印错误信息、记录日志或采取其他补救措施。
AI: 举一个处理 open()
系统调用错误的例子。
我: 当然,以下是一个处理 open()
系统调用错误的示例代码:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main() {
int fd = open("nonexistent_file.txt", O_RDONLY);
if (fd == -1) {
fprintf(stderr, "Error opening file: %s\n", strerror(errno));
return 1;
}
close(fd);
return 0;
}
在这个例子中,如果 open()
调用失败,fd
将被设置为 -1
,程序会检查 errno
并使用 strerror()
函数将错误代码转换为错误信息,然后打印错误信息并返回1表示程序异常退出。
知识点2:进程控制
AI: 什么是进程?
我: 进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己的地址空间、文件描述符、环境变量等。进程可以包含一个或多个线程,每个线程共享进程的资源。
AI: fork()
系统调用的返回值有什么含义?
我: fork()
系统调用的返回值有三种可能:
- 如果
fork()
成功,它会在父进程中返回子进程的PID(进程标识符),在子进程中返回0。 - 如果
fork()
失败,它会返回-1,并设置全局变量errno
以指示具体的错误原因。
AI: exec()
系统调用的变种有哪些?
我: exec()
系统调用有多个变种,它们的主要区别在于参数的传递方式:
execl()
: 以固定数量的参数调用新程序。execlp()
: 类似于execl()
,但会在环境变量PATH
中查找可执行文件。execle()
: 类似于execl()
,但允许传递环境变量。execv()
: 以数组形式传递参数。execvp()
: 类似于execv()
,但会在环境变量PATH
中查找可执行文件。execvpe()
: 类似于execvp()
,但允许传递环境变量。
AI: wait()
系统调用的参数和返回值是什么?
我: wait()
系统调用的原型如下:
pid_t wait(int *status);
- 参数
status
是一个指向整数的指针,用于接收子进程的退出状态。如果不需要获取退出状态,可以将status
设为NULL
。 - 返回值是子进程的PID。如果没有子进程结束,
wait()
会阻塞父进程。如果wait()
失败,它会返回-1,并设置全局变量errno
以指示具体的错误原因。
AI: 举一个使用 fork()
和 exec()
的示例。
我: 当然,以下是一个使用 fork()
和 exec()
的示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid());
execlp("/bin/ls", "ls", "-l", NULL);
fprintf(stderr, "Failed to execute ls\n");
return 1;
} else {
// 父进程
printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
wait(NULL); // 等待子进程结束
printf("Child process finished\n");
}
return 0;
}
在这个例子中,父进程调用 fork()
创建一个子进程。子进程调用 execlp()
执行 /bin/ls
命令,列出当前目录的详细信息。父进程调用 wait()
等待子进程结束,然后打印一条消息表示子进程已经结束。
学习实践过程遇到的问题与解决方式
问题1:fork()
调用后,子进程和父进程同时运行导致输出混乱
描述:在使用 fork()
创建子进程后,子进程和父进程同时运行,导致输出混乱,难以区分哪个输出来自哪个进程。
解决方式:
- 使用
sleep()
临时解决:#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "Fork failed\n"); return 1; } else if (pid == 0) { // 子进程 sleep(1); // 让子进程稍微延迟一下 printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid()); execlp("/bin/ls", "ls", "-l", NULL); } else { // 父进程 printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid); wait(NULL); // 等待子进程结束 printf("Child process finished\n"); } return 0; }
- 使用
wait()
确保子进程先结束:#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "Fork failed\n"); return 1; } else if (pid == 0) { // 子进程 printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid()); execlp("/bin/ls", "ls", "-l", NULL); } else { // 父进程 wait(NULL); // 等待子进程结束 printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid); printf("Child process finished\n"); } return 0; }
问题2:exec()
调用失败,程序无法执行新程序
描述:在调用 exec()
系列函数时,程序无法成功执行新程序,导致程序挂起或报错。
解决方式:
- 检查路径和文件名:
确保提供的路径和文件名是正确的。如果使用execlp()
或execvp()
,确保环境变量PATH
中包含了可执行文件的路径。 - 检查错误信息:
使用perror()
或strerror(errno)
打印详细的错误信息。#include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "Fork failed\n"); return 1; } else if (pid == 0) { // 子进程 execlp("/bin/ls", "ls", "-l", NULL); perror("Failed to execute ls"); // 打印错误信息 return 1; } else { // 父进程 wait(NULL); // 等待子进程结束 printf("Child process finished\n"); } return 0; }
问题3:open()
系统调用失败,无法打开文件
描述:在调用 open()
系统调用时,无法打开指定的文件,导致程序出错。
解决方式:
- 检查文件路径和权限:
确保文件路径是正确的,并且当前用户有权限访问该文件。 - 检查错误信息:
使用perror()
或strerror(errno)
打印详细的错误信息。#include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { int fd = open("nonexistent_file.txt", O_RDONLY); if (fd == -1) { fprintf(stderr, "Error opening file: %s\n", strerror(errno)); return 1; } close(fd); return 0; }
问题4:wait()
调用后,父进程无法正确捕获子进程的退出状态
描述:在调用 wait()
系统调用后,父进程无法正确捕获子进程的退出状态,导致程序逻辑出错。
解决方式:
使用 WIFEXITED
和 WEXITSTATUS
宏:
使用 WIFEXITED
检查子进程是否正常退出,使用 WEXITSTATUS
获取子进程的退出状态。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程
execlp("/bin/ls", "ls", "-l", NULL);
perror("Failed to execute ls");
return 1;
} else {
// 父进程
int status;
pid_t child_pid = wait(&status);
if (WIFEXITED(status)) {
printf("Child process %d exited with status %d\n", child_pid, WEXITSTATUS(status));
} else {
printf("Child process %d terminated abnormally\n", child_pid);
}
}
return 0;
}
问题5:环境变量传递失败,子进程无法获取正确的环境变量
描述:在使用 exec()
系列函数时,子进程无法获取正确的环境变量,导致程序出错。
解决方式:
检查环境变量的传递方式:
确保在调用 exec()
系列函数时正确传递了环境变量。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程
char *new_env[] = {"PATH=/usr/bin:/bin", "MY_VAR=value", NULL};
execle("/bin/ls", "ls", "-l", NULL, new_env);
perror("Failed to execute ls");
return 1;
} else {
// 父进程
wait(NULL); // 等待子进程结束
printf("Child process finished\n");
}
return 0;
}
参考资料
- HeadFirstC嗨翻C语言
- 通义千问
- Linux System Calls
- Process Control in Linux
- Error Handling in C
- 课程 mindmap
- Mermaid Live Editor