chapter5
自己键入并运行了下本章的代码
p1.c
代码:
运行结果:
书上的解释:
补充:
fork()
是用于创建子进程的系统调用。调用 fork()
后,会在父进程中创建一个与自己几乎完全相同的子进程,父进程和子进程从 fork()
之后的代码开始并行执行。fork()
是 UNIX 系统中实现并发操作的一个基础函数。
fork() 的返回值:
-
对于父进程:fork() 返回子进程的 PID(进程 ID),这个值是一个正整数。
-
对于子进程:fork() 返回 0。
-
如果 fork() 调用失败:返回 -1,此时不会创建子进程。
通过判断 fork() 的返回值,可以区分当前进程是父进程还是子进程,并让它们执行不同的代码。
p2.c
代码:
运行结果:
书上的解释:
补充:
wait()
函数用于使父进程暂停执行,直到它的一个子进程结束。这个函数会收集子进程的退出状态,以便父进程处理。
wait() 的功能:
-
等待子进程:
wait()
会使父进程暂停,直到有一个子进程结束(使父进程等待其任意一个子进程的结束)。如果父进程没有子进程,wait()
立即返回 -1。 -
返回子进程的 PID:
wait()
返回终止的子进程的 PID。 -
存储退出状态:
wait()
的参数是一个指针,它用于存储子进程的退出状态。如果父进程不关心退出状态,可以传递 NULL,即使用wait(NULL)
。
wait() 的返回值:
-
成功时:返回终止的子进程的 PID,并将子进程的退出状态存储在传入的指针中。
-
失败时:返回 -1,通常是因为当前没有可等待的子进程。
wait() 与子进程的退出状态:
wait()
可以通过指针参数获得子进程的退出状态,一般通过 WIFEXITED(status)
和 WEXITSTATUS(status)
来检查和获取退出码:
-
WIFEXITED(status)
:判断子进程是否正常退出。 -
WEXITSTATUS(status)
:获取子进程的退出码(在子进程中调用exit()
或return
返回的值)。
比如:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("Fork failed");
exit(1);
}
else if (pid == 0)
{
// 子进程
printf("This is the child process. PID: %d\n", getpid());
exit(42); // 子进程退出并返回 42
}
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));
}
}
return 0;
}
输出如下:
附上关于perror
和fprintf
的一篇博客:
更多的:
如果想阅读man手册来获取wait()
的更多细节,可使用man 2 wait
命令。
输入该命令并enter后,出现以下内容:
p3.c
代码
运行结果:
书上的解释:
exec()
系统调用,它也是创建进程 API 的一个重要部分。这个系统调用可以让子进程执行与父进程不同的程序。例如,在 p2.c 中调用 fork()
,这只是在你想运行相同程序的拷贝谁有用。但是,我们常常想运行不同的程序,exec()
正好做这样的事。
补充:
exec()
系列函数用于在当前进程中执行一个新的程序。调用 exec()
后,当前进程的映像被替换为新程序的映像,因此,exec()
成功后不会返回到原来的程序。
功能:
-
替换当前进程:
exec()
会用指定的程序替换当前进程的上下文,包括代码段、数据段和堆栈。此后,进程执行的新程序代码。 -
不返回:如果
exec()
调用成功,后续代码不会执行;如果失败,会返回到调用的位置,通常会产生一个错误。
exec()系列函数:
用man 3 exec
查看更多内容
附上一篇博客:exec()系列函数
p4.c
代码
运行结果:
书上的解释:
补充:
-
STDOUT_FILENO
是什么STDOUT_FILENO
是一个常量,通常在 C/C++ 编程中用于表示标准输出流的文件描述符。在 POSIX 系统(如 Linux 和 macOS)中,标准输出的文件描述符通常是 1。这个常量在<unistd.h>
头文件中定义。使用
STDOUT_FILENO
可以使代码更具可读性,避免硬编码数字。例如,使用write(STDOUT_FILENO, "Hello, World!\n", 14);
来向标准输出写入数据,而不是直接使用write(1, "Hello, World!\n", 14);
。 -
POSIX是什么
POSIX(可移植操作系统接口,Portable Operating System Interface)是一个由 IEEE(电气和电子工程师协会)制定的标准,旨在促进不同操作系统之间的兼容性和可移植性。
POSIX 定义了一系列操作系统 API(应用程序编程接口)、命令行工具和shell接口,确保程序可以在遵循 POSIX 标准的多个操作系统上运行,而不需要进行大幅修改。
许多现代操作系统(如 Linux、macOS 和一些 UNIX 变种,但windows不完全遵循)都遵循 POSIX 标准,因此开发者可以更容易地编写跨平台的代码。
-
S_IRWXU是什么
S_IRWXU 是一个常量,用于指定文件权限位中的用户权限。在 C/C++ 编程中,尤其是涉及到 POSIX 系统的文件操作时,文件权限通常以位掩码的形式表示。
定义:
S_IRWXU 是一个八进制值,通常等于 0700,表示用户(文件的所有者)对文件的读、写和执行权限。
各权限的具体值
-
S_IRUSR:读权限(0400)。
-
S_IWUSR:写权限(0200)。
-
S_IXUSR:执行权限(0100)。
组合:
将这些权限合并起来:
S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR = 0400 | 0200 | 0100 = 0700(八进制)。
S_IRWXU 实际上是这三个权限的组合,表示用户可以:
-
读文件。
-
写文件。
-
执行文件(如果是可执行文件)。
-
作业
第一题
问题:
编写一个调用 fork()的程序。在调用 fork()之前,让主进程访问一个变量(例如 x)并将其值设置为某个值(例如 100)。子进程中的变量有什么值?当子进程和父进程都改变x 的值时,变量会发生什么?
自己写的
输出如下:
子进程的变量一开始与父进程相同。但是实际上父子进程中的同名变量已经不在同一内存区域,实际上是两个变量,因此父子进程的变量值改变不会影响另外一个进程。
书上原话:
子进程并不是完全拷贝了父进程。具体来说,虽然它拥有自己的
地址空间(即拥有自己的私有内存)、寄存器、程序计数器等,但是它从 fork()返回的值是不同的。
说明子进程的内存区域已经和父进程的内存区域不同了,在内存中重新开辟了一个空间给子进程。
第二题
问题:
编写一个打开文件的程序(使用 open()系统调用),然后调用 fork()创建一个新进程。子进程和父进程都可以访问 open()返回的文件描述符吗?当它们并发(即同时)写入文件时,会发生什么?
自己写的
输出如下:
而且自动创建了一个2.txt
文件,内容为:
子进程和父进程都可以访问fd(fd是该程序中的一个文件描述符),但是存在竞争,无法同时使用fd,但最终都会写入成功。
第三题
问题:
使用 fork()编写另一个程序。子进程应打印“hello”,父进程应打印“goodbye”。你应该尝试确保子进程始终先打印。你能否不在父进程调用 wait()而做到这一点呢?
自己写的
输出如下:
使用vfork()
函数,可以在子进程结束后再执行父进程。
补充:
为什么使用vfork()
函数,可以在子进程结束后再执行父进程?
使用 vfork()
时,子进程会与父进程共享地址空间,直到子进程调用 exec()
或 _exit()
或exit()
(但是对于 _exit()
和exit()
一般vfork()
是和_exit()
搭配使用,exit()
在此处不建议使用,具体的原因请看这篇博客C中的open(), write(), close(), fopen(), exit(), _exit())。这是实现子进程执行结束后再执行父进程的原因。以下是具体的解释:
工作机制
-
共享地址空间:
当调用
vfork()
时,子进程并不会复制父进程的整个地址空间,而是共享它。这样可以节省内存开销。 -
执行顺序:
-
子进程被创建后,它会在父进程的上下文中执行。
-
子进程可以在其执行期间直接修改父进程的变量,但这通常是不安全的,因此应避免。
-
子进程在完成其任务后,必须调用
exec()
(替换为新程序)或_exit()
(终止自己)来结束其执行。只有在此之后,父进程才能继续执行。
-
阻塞行为:
vfork()
会使父进程阻塞,直到子进程结束。这意味着父进程在子进程执行期间不会继续运行。这个机制确保了在子进程完成其任务之前,父进程不会访问可能被修改的共享内存。
第四题
问题:
编写一个调用 fork()的程序,然后调用某种形式的 exec()来运行程序/bin/ls。看看是否可以尝试 exec()的所有变体,包括 execl()、execle()、execlp()、execv()、execvp()和 execvP()。为什么同样的基本调用会有这么多变种?
自己写的
输出如下:
这个其实是只执行execl(cmd, "ls", NULL)
的结果,因为exec系列函数不会返回,因此后面的代码不会执行。
我们可以通过注释不要的语句的的方式来查看其他的结果,此处不展示了。
可以看这篇博客exec()系列函数
同样的基本调用会有这么多变种是为了适应不同的调用形式和环境要求。
第五题
问题:
现在编写一个程序,在父进程中使用 wait(),等待子进程完成。wait()返回什么?如果你在子进程中使用 wait()会发生什么?
自己写的
输出如下:
对于父进程:fork()
返回子进程的 PID,对于子进程:fork()
返回0。
父进程使用wait()
返回终止的子进程的 PID,子进程本身没有子进程,所以返回-1。
第六题
问题:
对前一个程序稍作修改,这次使用 waitpid()而不是 wait()。什么时候 waitpid()会有用?
自己写的
输出如下:
对
waitpid
做个介绍
waitpid
是一个用于进程管理的系统调用,用于等待特定子进程的状态改变,通常是等待子进程结束。它提供了比 wait 更灵活的功能,允许你指定要等待的进程。
-
函数原型:
pid_t waitpid(pid_t pid, int *status, int options);
-
pid:要等待的进程的进程ID。如果为 -1,则等待任何子进程。如果为 0,则等待与调用进程同组的任意子进程。如果为正数,则等待指定的进程ID。
-
status:指向整数的指针,用于存储子进程的退出状态信息。如果不需要此信息,可以传递 NULL。
-
options:可以为0,或者设置一些选项,例如:
-
WNOHANG:非阻塞模式,如果没有子进程结束,则立即返回。
-
WUNTRACED:也返回那些被暂停的子进程的状态。
-
-
-
返回值
返回值是结束的子进程的进程ID。如果调用失败,返回 -1,并设置 errno。
-
举一个例子
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程执行 printf("Child process running...\n"); sleep(2); // 模拟一些工作 exit(42); // 返回一个退出状态 } else { // 父进程等待子进程 int status; pid_t result = waitpid(pid, &status, 0); if (result == -1) { perror("waitpid failed"); exit(EXIT_FAILURE); } // 检查子进程的退出状态 if (WIFEXITED(status)) { printf("Child exited with status: %d\n", WEXITSTATUS(status)); } } return 0; }
输出如下:
其中
WIFEXITED(status)
和WEXITSTATUS(status)
是用于检查子进程的退出状态的宏。
第七题
问题:
编写一个创建子进程的程序,然后在子进程中关闭标准输出(STDOUT_FILENO)。如果子进程在关闭描述符后调用 printf()打印输出,会发生什么?
自己写的
输出如下:
子进程里的printf语句没有打印出来。
第八题
问题:
编写一个程序,创建两个子进程,并使用 pipe()系统调用,将一个子进程的标准输出连接到另一个子进程的标准输入。
自己写的
有点长,截不全,就不截图了,直接给出代码
/* ************************************************************************
> File Name: 8.c
> Author: whq
> Created Time: 2024年10月30日 星期三 21时24分55秒
> Description: 《操作系统导论》第五章第八题
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
int fd[2];
int p = pipe(fd);
if (p < 0)
{
perror("pipe failed\n");
exit(1);
}
int i = 0;
int rc[2];
for (i = 0; i < 2; i ++ )
{
rc[i] = fork();
if (rc[i] < 0)
{
perror("fork failed\n");
exit(1);
}
else if(rc[i] == 0)
{
switch(i) {
case 0:
{
printf("I am child0 (pid:%d)\n", getpid());
char *msg = "hello, I am child0";
close(fd[0]);
write(fd[1], msg, sizeof(char) * strlen(msg));
return 1;
}
case 1:
{
printf("I am child1 (pid:%d)\n", getpid());
char asw[20];
close(fd[1]);
int res = read(fd[0], asw, sizeof(char) * 20);
printf("I am child1 and I get msg (size:%d, %s) from child0\n", res, asw);
return 2;
}
}
break;
}
else
{
int wc = waitpid(rc[i], NULL, 0);
printf("I am parent (pid:%d) of (pid:%d)\n", getpid(), wc);
}
}
waitpid(rc[1], NULL, 0);
return 0;
}
输出如下:
补充一下pipe的使用
pipe
函数用于在Unix/Linux系统中创建一个管道,它提供了一种进程间通信(IPC)机制,使得一个进程可以通过管道向另一个进程传递数据。管道是一个缓冲区,具有读写两端,数据写入管道一端后,可以从另一端读取。
-
函数原型
int pipe(int pipefd[2]);
pipefd
:一个长度为2的整型数组,用于存储管道的文件描述符。pipefd[0]
是读端的文件描述符,pipefd[1]
是写端的文件描述符。
-
返回值
如果成功,返回 0;如果失败,返回 -1,并设置 errno。
-
举一个例子
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe failed");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程:关闭写端,读取父进程发送的数据
close(pipefd[1]); // 关闭写端
char buffer[100];
read(pipefd[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(pipefd[0]); // 关闭读端
exit(0);
} else {
// 父进程:关闭读端,写数据到子进程
close(pipefd[0]); // 关闭读端
const char *message = "Hello from parent!";
write(pipefd[1], message, strlen(message) + 1); // +1 以包括 NULL 结束符
close(pipefd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
}
return 0;
}
输出如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通