目录
一、前言
今天开始做操作系统的实验,遇到了之前听过但是从未深入研究过的知识,所以今天打算对
涉及到的知识进行深入的总结,话不多说,开始吧!
二、fork()系统调用
fork()
是一个在 Unix 和类 Unix 系统中非常重要的系统调用,用于创建一个与当前进程
(父进程)几乎完全相同的新进程(子进程)。以下是 fork()
系统调用的详细说明:
2.1 函数原型
#include<unistd.h>
pid_t fork(void);
2.2 返回值
- 在父进程中,
fork()
返回新创建的子进程的 PID(一个大于 0 的整数)。 - 在子进程中,
fork()
返回 0。 - 如果
fork()
调用失败,返回-1
,并且errno
会被设置为相应的错误代码。
注意:这里容易出现误解点,如果fork()返回值大于0了,说明这个时候是在父进程里面,并且返
回的这个值是子进程的PID,因为子进程的PID一定是大于0的;如果fork()返回的是0,说明此时是
在子进程里面的,这一点一定要区分!!!
2.3 行为描述:
进程复制:fork()
创建了父进程的一个副本。子进程获得了父进程数据空间、堆、栈等资源
的一个副本。
资源复制:子进程继承了父进程的所有资源,包括打开的文件描述符、信号处理设置、定时器等。
独立执行:子进程从 fork()
返回点独立于父进程继续执行。这意味着子进程从 fork()
调
用之后的代码开始执行,而父进程则继续执行 fork()
调用之后的代码。
进程 ID:子进程被赋予一个新的、唯一的进程 ID(PID),而父进程保留其原始 PID。
父/子关系:父进程可以使用其 fork()
返回的 PID 来控制或监视子进程(例如,使用
wait()
等待子进程结束)。
内存空间:子进程获得父进程数据段、堆和栈的副本。但是,子进程的栈是独立的,这意味
着任何局部变量的修改都不会影响父进程
错误处理:如果 fork()
调用失败,可能是因为系统资源限制(如进程数量限制)或内存不
足。调用者应该检查 errno
来确定错误原因。
2.4 使用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// fork 失败
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
printf("I'm the child process with PID: %d\n", getpid());
} else {
// 父进程
printf("I'm the parent process, and my child has PID: %d\n", pid);
}
// 其他代码...
return 0;
}
三、pipe()系统调用
pipe()
是一个在 Unix 和类 Unix 系统中用于创建管道的系统调用。管道是一种特殊的文件
描述符对,允许两个进程之间的单向通信。以下是 pipe()
系统调用的详细说明:
3.1 函数原型
#include<unistd.h>
int pipe(int pipefd[2]);
从函数原型可以看出,传入的参数是一个包含两个元素的数组,pipefd
:一个指向整型数组的
指针,该数组至少有两个整数的空间。成功创建管道后,这个数组的两个元素将被设置为文件描述
符。分别代表着管道的写入端和读取端。
3.2 返回值
- 成功时,返回 0。
- 失败时,返回 -1,并设置
errno
以指示错误。
3.3 行为描述:
-
创建管道:
pipe()
创建一个管道,它是一个缓冲区,可以在一个写入端和一个读取端之间传递数据。 -
文件描述符对:
pipefd[0]
是管道的读取端,pipefd[1]
是管道的写入端。 -
单向通信:数据可以从写入端写入,并且只能从读取端读取。尝试反向操作将导致
EPIPE
错误。 -
缓冲区:管道有自己的缓冲区,数据写入写入端后,会存储在缓冲区中,直到被读取端读取。
-
进程间通信:管道通常用于父子进程或兄弟进程之间的通信。
-
关闭文件描述符:使用完管道后,应该关闭
pipefd[0]
和pipefd[1]
以释放资源。
注意:这里的管道是单向的,就是一方写,另一方只能读,假设父进程写入,那只能在子进程
接收这个写入的数据,由于是单向的,所以父进程这边不能接收子进程传过来的数据,同样子进程
那边也是不能写数据传入父进程,如果想要子进程写入数据传给父进程的话,也要创建一个通道,
供子进程写入,父进程接收!如下图所示例:
相信有很多人有误区,就是以为parentpipe[0]是父进程的读,即父进程读取数据的文件
描述符,这里就错了;首先我们要明白一件事,进程之间是通过管道传递数据的,所以只要用到了
管道,涉及到的管道就不可能是一个进程,至少有俩个,他们之间就相当于用了一根管子连接,所
以自然而然管道的两端面对的对象是不一样的。由于管道是单向的,所谓的单向就是,一边要是写
另一边只能是读,不存在一边既是写又是读的,这不符合定义。
3.4 使用示例:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main(){
int pipefd[2];
if(pipe(pipefd) == -1){
perror("pipe failed");
exit(EXIT_FAILURE);
}
char buff[20]="hello,pipe";
if(write(pipefd[1],buff,sizeof(buff) == -1){
perror("write failed");
exit(EXIT_FAILURE);
}
//关闭写入端,会触发一个信号槽机制,告知读取端可以读数据了
close(pipefd[1]);
if(read(pipefd[0],buff,sizeof(buff)) == -1){
perror("read failed");
exit(EXIT_FAILURE);
}
printf("read from pipefd[0] is %s\n",buff);
//关闭读取端,会触发一个信号机制,与上面同理
close(pipefd[0]);
return 0;
}
四、Mit6.s081(lab1)
通过上述讲的知识点,我们来做一个操作系统的实验,就是著名的操作系统实验
mit6.s081,要求是这样的:
实验的大致意思就是,要求写一个程序,使用UNIX系统调用在两个进程之间的一对管道上实现了"乒乓"通信。父进程向子进程发送一个字节;子进程应该打印"<pid>: received ping",其中<pid>是它的进程ID,将字节写入管道到父进程,并退出;父进程应该从子进程读取字节,打印"<pid>: received pong",然后退出。
这里首先要讲述以下我自己根据前面的知识点结合自己的想法实现的:首先通过系统调用fork()函数创建子进程,根据fork()函数返回的结果判断子进程是否创建成功,若子进程创建成功,则一开始子进程读取父进程传过来的数据,通过pipeparent[0]读取数据,但是在读取之前,为了防止父子进程同时读导致死锁,所以要先把父进程的读端口关闭,即pipechild[0]关闭,又因为管道是单向的,一端读了,另一端不能进行写了,所以要把父进程的写入端关闭;
输出结果为:
上述的难点还是在于理解父进程的写端口和读端口,父进程的写确实是父进程往管道的这个文件描述符写入数据,对应的文件描述符是pipeparent[1],但是pipeparent[0]是子进程读的文件描述符,
子进程通过系统调用函数read(),读取到了父进程传入的数据,这个过程涉及到变态,即用户态到内核态,会消耗大量的资源。