文章目录
1.管道小总结
linux-manualshouce
在Linux中,manual手册的编号用于区分手册的不同部分。这些编号通常用于man命令中,以便用户可以指定要查看的手册部分。man命令的编号对应关系如下:
用户命令(User Commands):通常包括可执行程序或shell命令。
系统调用(System Calls):由内核提供的函数。
库调用(Library Calls):程序库中的函数。
特殊文件(Special Files):通常位于/dev目录下的设备文件。
文件格式和约定(File Formats and Conventions):例如/etc/passwd等配置文件的格式。
游戏(Games)。
杂项(Miscellaneous):包括宏包和约定等其他内容。
系统管理命令(System Administration Commands and Daemons)。
此外,还有第9个部分,通常用于其他内容,比如内核例行程序(Kernel Routines)。
管道读写规则
- 当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。 - 当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN - 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
原子性
要么做要么不做。无中间状态。
多执行流下数据出现并发访问时考虑。
2.命名管道
2.1认识命名管道
- 进程A打卡了文件file,进程B再去打开file时,OS识别到file已打开,则不再创建对应的file结构体,而是直接指向已有的file结构体。【OS中一般都会存在n多个进程,且存在像许多进程打开同一个文件的情况,如果对于已经打开的文件,另外的进程再去打开他时区创建一个新的file结构体,这无疑是巨大的时空浪费】
- 命名管道无疑是这样的一个存在,首先他是一个文件,其次他不用将数据刷新到磁盘。
- 该文件一定在系统路径中(路径具有唯一性)。该文件有名字,可以被打开,但是不会将内存数据刷新到磁盘。
- 至此,两个毫不相干的进程通过命名管道文件的路径看到同一份资源。【匿名管道通常应用于有血缘关系的进程,这也是匿名管道的一个缺点】
2.2命名管道的应用小场景
创建命名管道
场景1
场景2:输出重定向
条件编译
#ifndef _COMMON_H_
#define _COMMON_H_
#endif
这三句代码是C或C++编程语言中常用的预处理器指令,它们通常用于头文件中以防止头文件内容的多次包含(多重定义问题)。我来详细解释每句代码的含义:
#ifndef COMMON_H
这是一个条件编译指令,它检查是否定义了名为_COMMON_H_的宏(macro)。如果_COMMON_H_没有被定义,那么紧随其后的代码(直到遇到#endif或另一个条件编译指令)会被编译器包含(或编译)。
2. #define COMMON_H
这行代码定义了一个名为_COMMON_H_的宏。一旦这个宏被定义,再次遇到#ifndef _COMMON_H_时,由于_COMMON_H_已经被定义,所以其后的代码不会被再次包含。
3. #endif
这是一个结束标记,表示#ifndef条件编译指令的结束。
将这三行代码放在头文件common.h的开始处,可以确保无论这个头文件被包含多少次,其内部的内容都只会被编译器编译一次。这在处理循环包含或者复杂项目结构时非常有用,因为你可以避免函数或变量被多次定义,从而避免编译错误。
这样,当common.h首次被包含时,其内容会被编译;如果再次被包含,由于_COMMON_H_已经被定义,其内容不会被再次编译。
- #ifndef/#define/#endif 是C和C++语言标准支持的预处理指令组合,用于实现头文件保护(header guards)的功能。它们通过宏定义来防止头文件被重复包含。
- #pragma once 则是编译器特定的指令,其工作原理依赖于编译器的实现。由于它是非标准的,因此在不同编译器之间的可移植性可能不如 #ifndef/#define/#endif 组合。
unlink()删除文件
rm和unlink的异同
在Linux中,rm和unlink都用于删除文件或目录,但它们在使用方式和功能上有一些重要的区别。
- 命令形式和使用方式:
rm 是一个更高级的、用户友好的命令,用于删除文件或目录。它可以处理多种复杂的情况,例如删除目录(使用 -r 或 -R 选项)或强制删除文件(使用 -f 选项)。
unlink 是一个较低级的系统调用,通常用于在编程中删除文件。在shell中,你可以使用 unlink 命令来删除文件,但它没有 rm 那么多的选项和功能。
2. 功能和行为:
在功能上,rm 和 unlink 基本上是一样的:它们都删除了指定的文件或目录。然而,由于 rm 提供了更多的选项,它可以更灵活地处理各种情况。
在行为上,rm 和 unlink 都减少了文件的链接计数。如果一个文件只有一个链接(即它是唯一被引用的),那么当这个链接被 rm 或 unlink 删除后,该文件占用的磁盘空间也会被释放。但是,如果一个文件有多个链接,那么删除其中一个链接只会减少链接计数,而不会释放磁盘空间。
3. 错误处理:
rm 在删除不存在的文件或目录时,会输出一个错误消息。然而,如果你使用 -f 选项,rm 会静默地忽略这些错误。
unlink 在遇到错误时,通常会返回一个错误代码,但具体的行为可能取决于你的shell和如何调用 unlink。
总的来说,rm 和 unlink 在功能上相似,但在使用方式和行为上有所不同。对于日常的文件删除任务,rm 通常是一个更好的选择,因为它提供了更多的选项和更友好的错误处理。而在编程中,你可能需要更直接地控制文件删除的过程,这时 unlink 可能更合适。
在Linux中,unlink
系统调用用于删除文件的链接。具体来说,unlink
可以对以下类型的文件进行操作:
拓展unlink可删除的文件
1. 普通文件
unlink
可以用于删除普通文件。当你调用unlink
删除一个普通文件时,该文件的链接计数会减一,如果没有剩余的链接(即引用计数为零),其空间将被释放。
2. 符号链接(Symbolic Links)
unlink
也可以用于删除符号链接。对于符号链接,unlink
会删除链接本身,但不会影响它所指向的目标文件。
3. 管道文件
- 对于命名管道(FIFO),
unlink
可以用来删除这些特殊文件。
4. 套接字(Sockets)
unlink
也适用于Unix域套接字,可以用于删除套接字文件。
5. 目录
- 虽然
unlink
可以用于目录项(移除目录的链接),但在大多数情况下,直接使用rmdir
来删除空目录是更常见的做法。unlink
不能直接删除非空目录。
注意事项
-
权限:在使用
unlink
删除文件时,必须具备对该文件的写权限以及对其父目录的执行权限。 -
不适用于打开的文件:如果文件仍然被进程打开,则可以通过
unlink
删除文件,但文件内容在所有引用都关闭之前仍然存在,直到最后一个引用被关闭。
总结
unlink
可以操作的文件类型包括普通文件、符号链接、命名管道和套接字文件等。但是,它不适合用于删除非空目录,且需要注意文件的权限设置。
2.3模拟命名管道
1.Log.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string tip[] ={
"Debug",
"Notice",
"Warning",
"Error"};
std::ostream &Log(std::string message, int option)
{
// 获取时间戳 time_t timestamp; time(timestamp);
time_t timestamp = time(nullptr);
if (timestamp == std::time_t(-1))
{
std::cerr << "获取时间失败" << std::endl;
exit(1);
}
// 获取格式化时间 tm *localtime(const time_t *__timer)
tm *timeinfo = std::localtime(×tamp);
std::cout << " | "
<< 1900 + timeinfo->tm_year << "-"
<< 1 + timeinfo->tm_mon << "-"
<< timeinfo->tm_mday << " "
<< timeinfo->tm_hour << ":"
<< timeinfo->tm_min << ":"
<< timeinfo->tm_sec
<< " | "
<< tip[option]
<< " | "
<< message;
return std::cout;
}
#endif
2.common.hpp
#ifndef _COMMON_H_
#define _COMMON_H_
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iomanip>
#include "Log.hpp"
using namespace std;
#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";
#endif
3.server.cxx
#include "common.hpp"
#include <sys/wait.h>
static void IpcWithClient(int fd)
{
char buffer[SIZE];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
// 【OS写的时候不写\0 这里读的时候自然没有\0 我们空一个以免有需要在末尾加0】
// 当然 上面的memset已经对所有单元都加了\0 这里只是通用的默认规范
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0) // read success
{
cout << "[" << getpid() << "] "
<< "client say:> " << buffer << endl;
}
else if (s == 0) // end of file
{
cerr << "[" << getpid() << "] "
<< "read end of file, clien quit, server quit too!" << endl;
break;
}
else // read error
{
perror("IpcWithClient::read");
break;
}
}
}
int main()
{
// 1. 创建管道文件 int mkfifo(const char *__path, mode_t __mode)
if (mkfifo(ipcPath.c_str(), MODE) < 0) //成功返回0 失败返回-1
{
perror("server::mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << " step 1" << endl;
// 2. 打开管道文件
int fd = open(ipcPath.c_str(), O_RDONLY);
if (fd < 0)
{
perror("server::open");
exit(2);
}
Log("打开管道文件成功", Debug) << " step 2" << endl;
// 3. 开始进行IPC 服务端创建子进程去与客户端进行ipc
int serverChildNum = 3;
for (int i = 0; i < serverChildNum; i++)
{
pid_t id = fork();
if (id == 0)
{
IpcWithClient(fd);
exit(1);
}
}
//阻塞式等待子进程 获取子进程退出状态 回收子进程
for (int i = 0; i < serverChildNum; i++)
{
waitpid(-1, nullptr, 0);
}
// 4. 关闭文件
close(fd);
Log("关闭管道文件成功", Debug) << " step 3" << endl;
unlink(ipcPath.c_str()); // int unlink(const char *__name)
Log("删除管道文件成功", Debug) << " step 4" << endl;
return 0;
}
4.client.cxx
#include "common.hpp"
int main()
{
// 1. 获取管道文件 创建文件由server负责
// 这里不加O_TRUNC 文件不存在client也不创建 失败是server的事情
int fd = open(ipcPath.c_str(), O_WRONLY);
if (fd < 0)
{
perror("client::open");
exit(1);
}
// 2. 进行ipc
string buffer;
while (true)
{
cout << "Please input information:> ";
//istream& getline<char, char_traits<char>, allocator<char>>(istream& __is, string& __str)
std::getline(std::cin, buffer);
write(fd, buffer.c_str(), buffer.size());
}
// 3. 关闭
close(fd);
return 0;
}
3.管道代码总结
- 模拟匿名管道: 父进程调用pipe() 创建匿名管道 记录pipefd 父子进程各自关闭不用的pipefd 之后父进程写到写端 子进程从读端读
- 模拟进程池: 父进程创建N个子进程 对每一个子进程之开启读端 父进程只开启写端 父进程随机选择一个子进程去执行它写到写端的命令 子进程阻塞等待获取从读端的命令 直到获取成功 否则一直阻塞等待
- 模拟命名管道:服务端进程创建命名管道 让子进程从fd里读信息后输出到显示器 客户端打开彼管道文件 向该fd写信息
4.管道练习题
- 典型进程间通信方式:管道,共享内存,消息队列,信号量。 网络通信,文件等多种方式。若说内存,则太过宽泛,并没有特指某种技术,不考虑。
- 进程之间具有独立性,拥有自己的虚拟地址空间,无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在什么位置)⇒ 进程之间不可以直接通过地址访问进行相互通信
- 进程间通信(1)通过内核中的缓冲区实现(2)文件以及网络通信
- 管道本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信。
使用int pipe(int pipefd[2])接口创建匿名管道,pipefd[0]用于从管道读取数据,pipefd[1]用于向管道写入数据。 - 管道特性:半双工通信,自带同步与互斥,生命周期随进程,提供字节流传输服务。在同步的提现中,若管道所有写段关闭,则从管道中读取完所有数据后,继续read会返回0,不再阻塞;若所有读端关闭,则继续write写入会触发异常导致进程退出
- 匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信
- 管道是半双工通信,是可以选择方向的单向通信。不可以实现双向数据传输
- 管道的本质是内核中的缓冲区,通过内核缓冲区实现通信,命名管道的文件虽然可见于文件系统,但是只是标识符,并非通信介质。管道实际上是一种固定大小的缓冲区,它存在于内存中,而不是磁盘上。因此,管道的容量大小并不是由磁盘容量大小来限制的
- 管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥⇒ 进程对管道进行读操作和写操作都可能被阻塞
- 多个进程只要能够访问同一管道就可以实现通信,不限于读写个数 ⇒ 一个管道只能有一个读进程或一个写进程对其操作⇒ 错误
- 管道的本质是内核中的缓冲区,命名管道文件是缓冲区的标识
- 管道在缓冲区写满后会写阻塞,跟磁盘空间并无关系
- 管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除管道文件后,之前已经打开管道的进程依然可以通信
- 管道的通信本质是通过内核中一块缓冲区(内存)实现数据传输,而命名管道的管道文件只是一个标识符,用于让多个进程能够访问同一块缓冲区
- 管道是半双工通信,是可以选择方向的单向通信
- 命名管道打开特性为,若以只读方式打开文件,则会阻塞,直到管道被以写的方式打开