第九周预习报告
文章目录
- 第九周预习报告
- 学习内容
- 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本周学习内容进程和系统调用章节概述系统调用定义常见系统调用openreadwritecloseforkexecwaitexit进程控制进程创建进程fork执行新程序exec等待子进程wait终止进程exit环境变量传递定义传递方式错误处理返回值errnoRSS订阅定义实现使用CURL库解析XML数据父子进程关系父子关系getppid重点内容系统调用定义常见系统调用openreadwritecloseforkexecwaitexit进程控制进程创建进程fork执行新程序exec等待子进程wait终止进程exit环境变量传递定义传递方式错误处理返回值errnoRSS订阅定义实现使用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