前言
ubuntu系统的默认用户名不为root的解决方案(但是不建议):轻量应用服务器 常见问题-文档中心-腾讯云 (tencent.com)
进程间通信的基本概念
进程间通信目的:进程间也是需要协同的,比如数据传输、资源共享、通知事件、进程控制
进程间通信的前提:让不同进程看到同一份OS中的资源(一段内存)
- 一定是某个进程提出了进程间通信的请求,OS才会创建一个共享资源
- 为了防止进程在通信时直接访问OS,OS会提供很多系统调用接口
- OS创建的共享资源的不同 + OS提供的系统调用接口的不同 = 进程间通信会有不同的种类
注意事项:
1、进程间能通信不等于能一直通信,fork函数子进程继承父进程不属于进程间通信
2、进程间通信的成本可能会稍微高一点
进程间通信依赖的标准:system V标准(主要用于本地通信) 和posix标准
system V标准规定的三种进程间通信的方案:消息队列、共享内存、信号量
管道
基本概念:System V 标准中提供了多种 IPC 机制,如消息队列、共享内存和信号量,但是使用这些 IPC 机制需要考虑很多细节问题(例如缓冲区大小、同步与互斥等),并且需要编写复杂的代码来实现,而管道只需一条简单命令即可创建,并且它们支持两个相关联地运行在同一系统上地程序之间互相传输信息(管道是最初人们实现进程间通信的一种方式)
问题:为什么父子进程会向同一个显示器终端打印数据?
解释:进行写操作时,父子进程都会向同一个内核级文件缓冲区中写入(内核级文件缓冲区不属于任何文件)当操作系统定期刷新时会将该缓冲区中的内容刷新到指定的文件中(在这里就是显示器文件)
问题:进程怎么做到默认打开三个标准流0、1、2?
解释:因为所有的进程都是bash的子进程,当bash打开(指向这三个流的文件,具体细节不再描述)了,bash的子进程也就打开了
问题:为什么子进程主动close(0或1或2)不影响父进程继续使用显示器文件呢?
解释:struct file中存在一个内存级的引用计数,父子进程同时指向一个struct file则该引用计数为2,close子进程的某个标准流文件时,只会将该引用计数减一,父进程依然可以访问(file - > ref_count--; if(ref_count == 0)才会释放文件资源)
问题:什么是管道文件?
解释:内核级文件缓冲区(重新设计后的) + struct file,父子进程一个负责向内核级文件缓冲区中写,另一个读取内核级文件缓冲区中的内容就形成了进程间通信的定义(让不同的进程看到同一份OS中的资源,文件系统也属于OS),此外为了保证父子进程间通信的合理性,管道文件只允许单向通信,同时读取会发生数据紊乱
补充:为了提高进程间通信的效率,避免写入文件缓冲区后还要向磁盘文件中刷新,所以OS设计者基于原来内核级文件缓冲区的代码,在OS中重新设计了一个不需要向磁盘中定时刷新的内核级文件缓冲区
问题:如何实现父进程读文件,子进程写文件?
解释:父进程仍然打开3号文件描述符close(4),子进程仍然打开4号文件描述符close(3)
问题:父子既然要关闭不需要的fd,为何之前还要打开?可以不关闭吗?
解释:①为了让子进程继承,通过继承后不用了再关闭的这种方式形成的进程间通信是由设计者深思熟虑后的结果(父进程只打开一个3后续子进程还要再关闭3再打开4,还不如父子进程都打开3和4按照实际情况再进行关闭,前者也可以但是后者更简单)②可以不关闭但是可能会造成父子进程同时写入,所以建议关闭,同时由于存放文件描述符的是一个数组,数组是有大小范围的,所以如果为了保证通信的单向性子进程让某个文件描述符空闲,如果还有其它情况造成的文件描述符在数组中处于空闲状态,就会造成文件描述符泄漏
匿名管道
pipe函数
函数原型:int pipe(int pipefd[2]);
包含头文件:<unistd.h>
参数:输出型参数,是一个由两个整数构成的数组,第一个元素表示读端的文件描述符,第二个元素表示写端的文件描述符,写端和读端的文件描述符由OS自行填写
返回值:调用成功返回0,否则返回-1
功能:在OS中创建一个用于进程间通信的没有名字的内核级文件缓冲区,即匿名管道
注意事项:
1、pipe函数的底层是open函数,只不过这里不需要提供文件路径、文件名以及初始权限
2、如果想要双向通信可以使用两个管道
3、将pipe创建的内核级文件缓冲区叫做管道,是因为它在本质上还是一个内核级文件缓冲区,只不过正常情况下一个进程对文件进行写的时候就是写入内核级文件缓冲区然后由OS负责定时刷新到管道,而新建的缓冲区不会向磁盘中刷新而是刷新给进程,刷新的目的地改变了,只需将原来内核级文件缓冲区的代码稍加更改就可以实现这一功能,并且“一进一出”还符合我们日常生活中对管道的理解
cfc
1、进程进程间通信是有成本的,需要做准备工作:
//2、创建子进程
pid_t id = fork();
if(id == 0)
{
//子进程---写端
//3、关闭不需要的fd
close(pipefd[0]);
close(pipefd[1]);//完成通信后也将子进程的写端关闭
exit(0);
}
//父进程---读端
close(pipefd[1]);
close(pipefd[0]); // 完成通信后也将子进程的读端关闭
2、进程间通信:
#include <iostream>
#include <unistd.h>
#include <cerrno> //c++版本的errno.h
#include <cstring> //c++版本的string.h
#include <sys/wait.h>
#include <sys/types.h>
#include <string>
// 携带发送的信息
std::string getOtherMessage()
{
// 获取要返回的信息
static int cnt = 0; // 计数器
std::string messageid = std::to_string(cnt);
cnt++; // 每使用一次计数器就++
pid_t self_id = getpid(); // 获取当前进程的pid
std::string stringpid = std::to_string(self_id);
std::string message = " my messageid is : ";
message += messageid;
message += " my pid is : ";
message += stringpid; // 逐渐向要传回的string字符串中追加要返回的信息
return message;
}
// 子进程进行写入
void ChildProcessWrite(int wfd)
{
std::string message = "father, I am your child process!";
while (true)
{
std::string info = message + getOtherMessage(); // 子进程尝试向父进程传递的所有信息
write(wfd, info.c_str(), info.size()); // write函数传入的字符串需要是c语言格式的,c_str将string字符串变为c语言格式的字符串
sleep(1); // 让子进程写慢一点,这样父进程就不会一直读并打印在显示器上
} // write是由操作系统提供的接口,而操作系统又是C语言编写的,所以后续学习中可能会碰到c语言的接口和c++的接口混合使用的情况
} // info最后有/0但是文件不需要
const int size = 1024; // 定义父进程可以读取的数组大小
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size]; // 普通的c99标准不支持变长数组,但是这里使用的是gnb的c99标准,gun的c99标准支持变长数组
while (true)
{
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer)); // 因为文件不需要\0,所以读取管道中内容到缓冲区时可以少读取一个并将/0变为0
if (n > 0)
{
inbuffer[n] = 0;
std::cout << "父进程获取的消息: " << inbuffer << std::endl;
}
}
}
int main()
{
// 1、创建管道
int pipefd[2];
int n = pipe(pipefd); // 输出型参数,rfd,wfd
if (n != 0)
{
std::cerr << "errno" << errno << ":" << "errstring" << strerror(errno) << std::endl;
return 1;
}
// pipefd[0]即读端fd,pipefd[1]即写端fd
std::cout << "pipefd[0] = " << pipefd[0] << ", pipefd[1] = " << pipefd[1] << std::endl;
sleep(1); // 便于看到管道创建成功
// 2、创建子进程
pid_t id = fork();
if (id == 0)
{
std::cout << "子进程关闭不需要的fd,准备发消息了" << std::endl;
sleep(1); // 便于感受到发消息的过程
// 子进程---写端
// 3、关闭不需要的fd
close(pipefd[0]);
ChildProcessWrite(pipefd[1]); // 子进程的写函数
close(pipefd[1]); // 完成通信后也将子进程的写端关闭
exit(0);
}
std::cout << "发进程关闭不需要的fd,准备收消息了" << std::endl;
sleep(1); // 便于感受到收消息的过程
// 父进程---读端
close(pipefd[1]);
FatherProcessRead(pipefd[0]); // 父进程的读函数
close(pipefd[0]); // 完成通信后也将子进程的读端关闭
pid_t rid = waitpid(id, nullptr, 0);
if (rid > 0)
{
std::cout << "wait child process done" << std::endl;
}
return 0;
}
结论:因为可以用write和read读取管道,所以管道也是文件
管道的四种情况
1、如果管道内部为空,不具备读取条件,读进程会被阻塞(wait)等到管道不为空时才会读取
2、管道被写满 && rfd不关闭也不读取:此时管道会被写满,写进程会被阻塞,等到管道不为满时才会继续写入
3、管道一直在读 && wfd关闭:读端read函数的返回值最后为0,表示读取到了文件结尾
4、rfd直接关闭 && 写端一直入:写端进程会被OS直接用13号信号杀掉(OS判断出进程异常)
管道的五种特征
1、对于匿名管道:只能用来进行具有“血缘关系”的进程间的通信,但常用于父子进程间通信
2、管道内部自带进程之间的同步机制(子进程写一条写父进程读一条(但也不绝对),管道在实现时内部做了保护,不会出现多进程同时访问共享资源导致的共享区数据不一致问题)
3、管道文件按的生命周期是随进程的
4、管道文件在通信的时候,是面向字节流的,读写次数不一定是一一匹配的(写十次一次一条,读一次一次读十条,水管一直流,但是可以选用不同的容器去接水)
5、管道的通信模式,是一种特殊的半双工模式(正常的半双工是双方都写入和接收,但同时只能有一个人写入另一个人负责接收,管道是永远只能有一个人进行写入另一个人进行接收)
~over~