Linux系统编程—进程间通信

第一章:进程间通信介绍

1-1 进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1-2 进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

1-3 进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

第二章:管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

第三章:匿名管道

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

3-1 实例代码

//例⼦:从键盘读取数据,写⼊管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
	int fds[2];
	char buf[100];
	int len;
	if (pipe(fds) == -1)
		perror("make pipe"), exit(1);
	// read from stdin
	while (fgets(buf, 100, stdin)) {
		len = strlen(buf);
		// write into pipe
		if (write(fds[1], buf, len) != len) {
			perror("write to pipe");
			break;
		}
		memset(buf, 0x00, sizeof(buf));
		// read from pipe
		if ((len = read(fds[0], buf, 100)) == -1) {
			perror("read from pipe");
			break;
		}
		// write to stdout
		if (write(1, buf, len) != len) {
			perror("write to stdout");
			break;
		}
	}
}

3-2 用 fork 来共享管道原理

匿名管道原理

匿名管道只能用于有血缘关系的进程

3-3 站在文件描述符角度-深度理解管道

3-4 站在内核角度-管道本质

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。

3-5 管道样例

3-5-1 测试管道读写

#include <iostream>
#include <stdio.h>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

#define N 2
#define NUM 1024

void writer(int wfd) {
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;
    
    char buffer[NUM];
    while (true) {
        sleep(1);
        buffer[0] = 0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当做字符串
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
        //发送/写入给父进程
        write(wfd, buffer, strlen(buffer));
        
        //读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
        // char c = 'c';
        // write(wfd, &c, 1); 
        // number++;
        //  if(number >= 5) break;
        // cout << number << endl;
    } 
}

void Reader(int rfd) {
    char buffer[NUM];
    int cnt = 0;
    while (true) {
        // sleep(1);
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if (n > 0) {
            buffer[n] = 0;
            cout << "fater get a message [" << getpid() << "]# " << buffer << endl;
        } 
        else if (n == 0) {
            cout << "father read file done" << endl;
            break;
        }
        else break;

        //写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程。
        cnt++;
        if (cnt > 5) break;
        // cout << "n: " << n << endl;    
    }
}


int main() {
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if (n < 0) return 1;

    //child -> w  father -> r
    pid_t id = fork();
    if (id < 0) return 2;
    if (id == 0) {
        close(pipefd[0]);//子进程写,所以关闭读
        //IPC code
        writer(pipefd[1]);
        close(pipefd[1]);//子进程通信完关闭写
        exit(0);        
    } 
    close(pipefd[1]);//父进程读,所以关闭写
    Reader(pipefd[0]);//读5秒
    close(pipefd[0]);//父进程通信完关闭写
    cout << "father close read fd: " << pipefd[0] << endl;
    sleep(3);//为了观察僵尸
    
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid < 0) return 3;
    cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) 
    << " exit signal " << ((status&0x7F)) << endl;

    sleep(3);
    cout << "father quit" << endl;
    return 0;
}

读写端正常,管道如果为空,读端就要阻塞

读写端正常,管道如果被写满,写端就要阻塞

管道的特征:

  1. 具有血缘关系的进程进行进程间通信
  2. 管道只能单向通信
  3. 父子进程是会进程协同的,同步与互斥的保护管道文件的数据安全
  4. 管道是面向字节流的
  5. 管道是基于文件的,而文件的生命周期是随进程的

管道的4种情况:

  1. 读写端正常,管道如果为空,读端就要阻塞
  2. 读写端正常,管道如果被写满,写端就要阻塞
  3. 读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
  4. 写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程。如何干掉?通过信号杀掉操作系统是不会做,低效,浪费等类似的工作的。如果做了,就是操作系统的bug。

管道是有固定大小的,多少?不同的内核里,大小可能有差别。

3-5-2 创建进程池处理任务

Task.hpp

#pragma once

#include <iostream>
#include <vector>
using namespace std;

typedef void(*task_t)();

void task1() { cout << "lol 刷新日志" << endl; }
void task2() { cout << "lol 更新野区,刷新野怪出来" << endl; }
void task3() { cout << "lol 检测软件是否更新,如果需要,就提示用户" << endl; }
void task4() { cout << "用户施放技能,更新用户的血量和蓝量" << endl; }

void LoadTask(vector<task_t>* tasks) {
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

ProcessPool.cc

#include "Task.hpp"
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>


const int processnum = 10;
vector<task_t> tasks;

//先描述
class channel { //对「管道写端 + 子进程信息」的封装
public:
    channel(int cmdfd, int slaverid, const string& processname)
        :_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname) {     
    }
public:
    int _cmdfd; //发送任务的文件描述符
    int _slaverid; //子进程的PID
    string _processname; //子进程的名字,方便打印日志
};

void slaver() {
    while (true) {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int));
        if (n == sizeof(int)) {
            cout << getpid() << " slaver say get a command, cmdcode:" << cmdcode << endl;
            if (cmdcode >= 0 && cmdcode < tasks.size())
                tasks[cmdcode]();//tasks是vector,每个元素是函数指针
        }
        if (n == 0) break;
    }
}

// //输入:const &
// //输出:*
// //输入输出:&
// //版本一有一个小bug:父进程在循环中不断创建新的管道。fork() 之后,子进程会继承当时父进程还没关闭的所有写端。
// void InitProcessPool(vector<channel>* channels) { 
//     for (int i = 0; i < processnum; i++) {
//         int pipfd[2];
//         int n = pipe(pipfd);
//         assert(!n);//检查 pipe() 是否成功(返回 0 才算成功)。
//         (void)n;//避免编译器提示 n 未使用的警告。
        
//         pid_t id = fork();
//         if (id == 0) { //child
//             close(pipfd[1]);
//             dup2(pipfd[0], 0);//为了slaver不传参设计重定向,pipfd[0]拷贝给0,slaver内部直接从0读
//             close(pipfd[0]);
//             slaver();
//             cout << "process:" << getpid() << " quite" << endl;
//             exit(0);
//         }
//         //father
//         close(pipfd[0]);
//         //添加channel字段
//         string name = "process-" + to_string(i);
//         channels->push_back(channel(pipfd[1], id, name));//构造并保存一个管道对象
//         //每个对象封装了“子进程的身份(pid+名字)+ 父进程能写给它的管道写端”。
//     }
// }

//版本二:确保每一个子进程只有一个写端
void InitProcessPool(vector<channel>* channels) { 
    vector<int> oldfds;//用来记录父进程中历史上创建过的管道写端
    for (int i = 0; i < processnum; i++) {
        int pipfd[2];
        int n = pipe(pipfd);
        assert(!n);//检查 pipe() 是否成功(返回 0 才算成功)。
        (void)n;//避免编译器提示 n 未使用的警告。
           
        pid_t id = fork();
        if (id == 0) { //child
            cout << "child:" << getpid() << " close history fd:";
            for (auto& fd : oldfds) { //把父进程中保存的所有旧写端关闭掉
                cout << fd << " ";
                close(fd);
            }
            cout << "\n";
            
            close(pipfd[1]);
            dup2(pipfd[0], 0);//为了slaver不传参设计重定向,pipfd[0]拷贝给0,slaver内部直接从0读
            close(pipfd[0]);
            slaver();
            cout << "process:" << getpid() << " quite" << endl;
            exit(0);
        }
        //father
        close(pipfd[0]);
        //添加channel字段
        string name = "process-" + to_string(i);
        channels->push_back(channel(pipfd[1], id, name));//构造并保存一个管道对象
        //每个对象封装了“子进程的身份(pid+名字)+ 父进程能写给它的管道写端”。
        oldfds.push_back(pipfd[1]);//父进程这次新建管道得到的写端保存到 oldfds,以便在下一轮循环前关闭。
        sleep(1);
    }
}

void Debug(const vector<channel>& channels) {
    //test
    for(const auto& c : channels)
        cout << "rfd:" << c._cmdfd << " " << "PID:"<< c._slaverid << " " << "processname:" << c._processname << endl;
}

void Menu() {
    cout << "#################################################" << endl;
    cout << "# 1.刷新日志               2.刷新野怪             #" << endl;
    cout << "# 3.检测软件是否更新       4.更新用户的血量和蓝量   #" << endl;
    cout << "#                    0.exit                     #" << endl;
    cout << "#################################################" << endl;
 }
void ctrlSlaver(const vector<channel>& channels) {
    // 手动指派版本(均衡负载)
    int which = 0;
    while (true) {
        //1. 选择任务
        int select = 0;
        Menu();
        cout << "Please Enter:";
        cin >> select;
        if (select <= 0 || select > 5) break;
        int cmdcode = select - 1;
        //2. 选择进程

        cout << "father say cmdcode:" << cmdcode << " already sent to " << channels[which]._slaverid 
        << " process name :" << channels[which]._processname << endl;
        //3. 发送任务
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));

        which++;
        which %= channels.size();
    }
    
    // // 顺序选择进程版本(均衡负载)
    // int which = 0;
    // int cnt = 5;
    // while (cnt) {
    //     //1. 选择任务
    //     int cmdcode = rand() % tasks.size();
    //     //2. 选择进程

    //     cout << "father say cmdcode:" << cmdcode << " already sent to " << channels[which]._slaverid 
    //     << " process name :" << channels[which]._processname <<endl;
    //     //3. 发送任务
    //     write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));

    //     which++;
    //     which %= channels.size();
    //     cnt--;
    //     sleep(1);
    // }

    // 随机选择进程版本
    // for (int i = 1; i <= 100; i++) {
    //     //1. 选择任务
    //     int cmdcode = rand() % tasks.size();
    //     //2. 选择进程
    //     int processpos = rand() % channels.size();

    //     cout << "father say cmdcode:" << cmdcode << " already sent to " << channels[processpos]._slaverid 
    //     << " process name :" << channels[processpos]._processname <<endl;
    //     //3. 发送任务
    //     write(channels[processpos]._cmdfd, &cmdcode, sizeof(cmdcode));
    //     sleep(1);
    // }
}

// //适配InitProcessPool版本一
// void QuitProcess(const vector<channel>& channels) {
//     // //能够正常工作,因为父进程先关闭所有写端,最后一个子进程退出后通过链式反应,所有子进程能够依次退出。
//     // for(const auto& c : channels) close(c._cmdfd);
//     // for(const auto& c : channels) waitpid(c._slaverid, nullptr, 0);

//     //倒着回收
//     int last = channels.size() - 1;
//     for (int i = last; i >= 0; i--) {
//         close(channels[i]._cmdfd);
//         waitpid(channels[i]._slaverid, nullptr, 0);
//     }
// }

//适配InitProcessPool版本二
void QuitProcess(const vector<channel>& channels) {
    ////如果适配InitProcessPool版本一
    ////父进程会为每个子进程创建管道,并且在每个子进程 fork 后,子进程会继承父进程的文件描述符(包括管道的写端)。
    ////如果父进程在创建子进程时没有及时关闭管道的写端,那么这些写端会被继承给所有子进程
    ////由于父进程关闭写端后立刻调用 waitpid,导致子进程继承父进程的写端,从而多个子进程的写端指向同一个管道,最终导致阻塞。
    for(const auto& c : channels) {
        close(c._cmdfd);
        waitpid(c._slaverid, nullptr, 0);
    }
}

int main() {
    LoadTask(&tasks);
    srand(time(nullptr)^getpid()^1023);
    vector<channel> channels;//再组织
    //1. 初始化
    InitProcessPool(&channels);
    Debug(channels);
    
    //2. 开始控制子进程
    ctrlSlaver(channels);

    //3. 清理收尾
    QuitProcess(channels);

    return 0;
}

3-6 管道读写规则

当没有数据可读时

  • 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将不再保证写入的原子性。

3-7 管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

第四章:命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

4-1 创建一个命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

$ mkfifo filename

命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *filename,mode_t mode);

4-2 匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

4-3 命名管道的打开规则

如果当前打开操作是为读而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

实例 用命名管道实现server&client通信

comm.hpp

#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum {
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init {
public:
    Init() {
        //创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1) {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }

    ~Init() {
        int m = unlink(FIFO_FILE);
        if (m == -1) {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

log.hpp

#pragma once

#include <iostream>
#include <stdarg.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

using namespace std;

#define SIZE 1024

//表示不同严重程度
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1 //打印到屏幕
#define Onefile 2 //所有日志写入同一个文件
#define Classfile 3 //按日志级别分文件

#define LogFile "log.txt"

class Log {
public:
    //默认输出到屏幕,日志目录为 ./log/
    Log() { 
        printMethod = Screen; 
        path = "./log/";
    }
    void Enable(int method) { printMethod = method; } //设置日志输出方式

    string levelToString(int level) { //把数字型的日志级别 → 转成人类可读的字符串
        switch (level) {
        case Info: return "Info";
        case Debug: return "Debug";
        case Warning: return "Warning";
        case Error: return "Error";
        case Fatal: return "Fatal";
        default: return "None";    
        }
    }

    void printLog(int level, const string& logtxt) { //根据 printMethod 的值决定日志去哪
        switch (printMethod) {
        case Screen:
            cout << logtxt << endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }

    void printOneFile(const string& logname, const string& logtxt) {
        string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY|O_CREAT|O_APPEND, 0666);
        if (fd < 0 ) return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    void printClassFile(int level, const string& logtxt) {
        string filename = LogFile;
        filename += ".";
        filename += levelToString(level);//log.txt.Debug/...
        printOneFile(filename, logtxt);
    }

    void operator()(int level, const char* format, ...) {
        //获取当前时间 → 格式化到 leftbuffer
        time_t t = time(nullptr);
        struct tm* ctime = localtime(&t);
        
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
        ctime->tm_year+1900, ctime->tm_mon+1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        //处理用户传入的可变参数 → 格式化到 rightbuffer
        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        //拼接成完整日志字符串
        char logtxt[SIZE*2];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
        //格式:默认部分+自定义部分

        // printf("%s", logtxt);
        printLog(level, logtxt);    
    }

    ~Log() {}
private:
    int printMethod;
    string path;
};

client.cc

#include "comm.hpp"

int main() {
    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd < 0 ) {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }


    cout << "client open file done" << endl;
    string line;
    while (true) {
        cout << "Please Enter:";
        getline(cin, line);
        write(fd, line.c_str(), line.size());
    }

    close(fd);
    return 0;
}

server.cc

#include "comm.hpp"
#include "log.hpp"

int main() {
    
    Init init;
    Log log;
    // log.Enable(Onefile);
    log.Enable(Classfile);

    //打开管道
    int fd = open(FIFO_FILE, O_RDONLY);//等待写入方打开之后,自己才会打开文件,向后执行。open阻塞
    if (fd < 0) {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

    log(Info, "server open file done. error string: %s, error code: %d", strerror(errno), errno);
    log(Warning, "server open file done. error string: %s, error code: %d", strerror(errno), errno);
    log(Fatal, "server open file done. error string: %s, error code: %d", strerror(errno), errno);
    log(Debug, "server open file done. error string: %s, error code: %d", strerror(errno), errno);
    log(Error, "server open file done. error string: %s, error code: %d", strerror(errno), errno);

    //开始通信
    while (true) {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if (x > 0) {
            buffer[x] = 0;
            cout << "client say: " << buffer << endl;
        }
        else if (x == 0) {
            log(Debug, "client quit, me too! error string: %s, error code: %d", strerror(errno), errno);
            break;
        }
        else break;
    }

    close(fd);
    return 0;
}

第五章:共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

5-1 共享内存示意图   

5-2 共享内存数据结构

struct shmid_ds {
	struct ipc_perm shm_perm; /* operation perms */
	int shm_segsz; /* size of segment (bytes) */
	__kernel_time_t shm_atime; /* last attach time */
	__kernel_time_t shm_dtime; /* last detach time */
	__kernel_time_t shm_ctime; /* last change time */
	__kernel_ipc_pid_t shm_cpid; /* pid of creator */
	__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
	unsigned short shm_nattch; /* no. of current attaches */
	unsigned short shm_unused; /* compatibility */
	void* shm_unused2; /* ditto - used by DIPC */
	void* shm_unused3; /* unused */
};

5-3 共享内存函数

shmget函数

功能:用来创建共享内存
原型
	int shmget(key_t key, size_t size, int shmflg);
参数
	key : 这个共享内存段名字
	size : 共享内存大小
	shmflg : 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
		取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
		取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回。
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回 - 1

shmat函数

功能:将共享内存段连接到进程地址空间
原型
	void* shmat(int shmid, const void* shmaddr, int shmflg);
参数
	shmid : 共享内存标识
	shmaddr : 指定连接的地址
	shmflg : 它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回 - 1

说明:

  • shmaddr为NULL,核心自动选择一个地址
  • shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
  • shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
  • shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

shmdt函数

功能:将共享内存段与当前进程脱离
原型
	int shmdt(const void* shmaddr);
参数
	shmaddr : 由shmat所返回的指针
返回值:成功返回0;失败返回 - 1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

shmctl函数

功能:用于控制共享内存
原型
	int shmctl(int shmid, int cmd, struct shmid_ds* buf);
参数
	shmid : 由shmget返回的共享内存标识码
	cmd : 将要采取的动作(有三个可取值)
	buf : 指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回 - 1

实例1. 共享内存实现通信

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "log.hpp"

using namespace std;

//共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2
const int size = 4096;
const string pathname = "/home/sxy";
const int proj_id = 0x6666;
Log log;

key_t GetKey() {
    key_t k = ftok(pathname.c_str(), proj_id);//获取key
    if (k < 0) {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is: 0x%x", k);
    return k;
}

int GetShareMemHelper(int flag) {
    key_t k = GetKey();
    int shmid = shmget(k, size, flag);//创建共享内存
    if (shmid < 0) {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid:%d", shmid);
    return shmid;
}

int CreateShm() { return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666); }
int GetShm() { return GetShareMemHelper(IPC_CREAT); }

processa.cc

#include "comm.hpp"

extern Log log;//复用之前命名管道的
int main() {
    int shmid = CreateShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接


    //一旦有人把数据写入到共享内存,立马就能看到。
    //不需要经过系统调用,直接就能看到数据
    while (true) {
        cout << "client say@ " << shmaddr << endl;
        sleep(1);
    }

    shmdt(shmaddr);//去关联
    shmctl(shmid, IPC_RMID, nullptr);//删除共享内存

    return 0;
}

processb.cc

#include "comm.hpp"

int main() {
    int shmid = GetShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接

    //一旦有了共享内存,挂接到自己的地址空间中,直接把它当成自己的内存空间来用即可。
    //不需要调用系统调用
    while (true) {
        cout << "Please Enter: ";
        fgets(shmaddr, 4096, stdin);
    }

    shmdt(shmaddr);//去关联

    return 0;
}

实例2. 借助管道实现访问控制版的共享内存(bug, 仅为理解,让进程执行产生一定的顺序性)

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "log.hpp"

using namespace std;

//共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2
const int size = 4096;
const string pathname = "/home/sxy";
const int proj_id = 0x6666;
Log log;

key_t GetKey() {
    key_t k = ftok(pathname.c_str(), proj_id);//获取key
    if (k < 0) {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is: 0x%x", k);
    return k;
}

int GetShareMemHelper(int flag) {
    key_t k = GetKey();
    int shmid = shmget(k, size, flag);//创建共享内存
    if (shmid < 0) {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid:%d", shmid);
    return shmid;
}

int CreateShm() { return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666); }
int GetShm() { return GetShareMemHelper(IPC_CREAT); }


#define FIFO_FILE "./myfifo"
#define MODE 0664

enum {
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init {
public:
    Init() {
        //创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1) {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }

    ~Init() {
        int m = unlink(FIFO_FILE);
        if (m == -1) {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

#endif

processa.cc

#include "comm.hpp"

extern Log log;
int main() {
    Init init;
    int shmid = CreateShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接

    int fd = open(FIFO_FILE, O_RDONLY);//等待写入方打开之后,自己才会打开文件,向后执行。open阻塞
    if (fd < 0) {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

    //一旦有人把数据写入到共享内存,立马就能看到。
    //不需要经过系统调用,直接就能看到数据
    struct shmid_ds shmds;
    while (true) {
        char c;
        ssize_t s = read(fd, &c, 1);
        if (s == 0) break;
        else if (s < 0) break;

        cout << "client say@ " << shmaddr << endl;
        sleep(1);

        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size " << shmds.shm_segsz << endl;
        cout << "shm nattch " << shmds.shm_nattch << endl;
        printf("shm __key 0x%x\n", shmds.shm_perm.__key);
        cout << "shm mode " << shmds.shm_perm.mode << endl;   
    }

    shmdt(shmaddr);//去关联
    shmctl(shmid, IPC_RMID, nullptr);//删除共享内存
    close(fd);
    return 0;
}

processb.cc

#include "comm.hpp"

int main() {
    int shmid = GetShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接
 
    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd < 0) {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

    //一旦有了共享内存,挂接到自己的地址空间中,直接把它当成自己的内存空间来用即可。
    //不需要调用系统调用
    while (true) {
        cout << "Please Enter: ";
        fgets(shmaddr, 4096, stdin);
        write(fd, "c", 1);//通知对方
    }

    shmdt(shmaddr);//去关联
    close(fd);
    return 0;
}

第六章:system V 消息队列

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • 特性方面
    • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

 第七章:system V 信号量

信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。

7-1 并发编程,概念铺垫

  • 多个执行流(进程), 能看到的同一份公共资源:共享资源
  • 被保护起来的共享资源叫做临界资源
  • 保护的方式常见:互斥与同步
  • 任何时刻,只允许一个执行流访问资源,叫做互斥
  • 多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
  • 在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(非临界区)
  • 所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护

7-2 信号量

特性方面

  • IPC资源必须删除,否则不会自动清除,除非重启,所以System V IPC资源的生命周期随内核

理解方面

  • 信号量是一个计数器

作用方面

  • 保护临界区

本质方面

  • 信号量本质是对资源的预订机制

操作方面

  • 申请资源,计数器--,P操作
  • 释放资源,计数器++,V操作

第八章:内核是如何组织管理IPC资源的

IPC资源标识符与数组索引解析

基于kern_ipc_perm的多态实现

作业

1. 以下选项属于进程间通信的是()[多选]

A.管道
B.套接字
C.内存
D.消息队列

答案:ABD
典型进程间通信方式:管道,共享内存,消息队列,信号量。 除此之外还有网络通信,以及文件等多种方式
C选项,这里的内存太过宽泛,并没有特指某种技术,错误。

2. 以下描述正确的有:        

A.进程之间可以直接通过地址访问进行相互通信
B.进程之间不可以直接通过地址访问进行相互通信
C.所有的进程间通信都是通过内核中的缓冲区实现的
D.以上都是错误的

答案:B
A错误:进程之间具有独立性,拥有自己的虚拟地址空间,因此无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在什么位置)
B正确
C错误:除了内核中的缓冲区之外还有文件以及网络通信的方式可以实现

3. 以下关于管道的描述中错误的是  [多选]

A.可以通过int pipe(int pipefd[2])接口创建匿名管道,其中pipefd[0]用于从管道中读取数据
B.可以通过int pipe(int pipefd[2])接口创建匿名管道,其中pipefd[0]用于向管道中写入数据
C.若在所有进程中将管道的写端关闭,则从管道中读取数据时会返回-1;
D.管道的本质是内核中的一块缓冲区;

答案:BC

  • 管道本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信。
  • 使用int pipe(int pipefd[2])接口创建匿名管道,pipefd[0]用于从管道读取数据,pipefd[1]用于向管道写入数据。
  • 管道特性:半双工通信,自带同步与互斥,生命周期随进程,提供字节流传输服务。
  • 在同步的提现中,若管道所有写段关闭,则从管道中读取完所有数据后,继续read会返回0,不再阻塞;若所有读端关闭,则继续write写入会触发异常导致进程退出

根据以上管道理解分析:A正确,B错误,C错误,D正确
因为题目为选择错误选项,因此选择B和C选项

4. 以下关于管道的描述中,正确的是 [多选]

A.匿名管道可以用于任意进程间通信
B.匿名管道只能用于具有亲缘关系的进程间通信
C.在创建子进程之后也可以通过创建匿名管道实现父子进程间通信
D.必须在创建子进程之前创建匿名管道才能实现父子进程间通信

答案:BD
A错误,匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以用于同一主机上的任意进程间通信
B正确
C错误,匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信
D正确

5. 下列关于管道(Pipe)通信的叙述中,正确的是() 

A.一个管道可以实现双向数据传输
B.管道的容量仅受磁盘容量大小限制
C.进程对管道进行读操作和写操作都可能被阻塞
D.一个管道只能有一个读进程或一个写进程对其操作

答案:C
A错误 管道是半双工通信,是可以选择方向的单向通信      
B错误 管道的本质是内核中的缓冲区,通过内核缓冲区实现通信,命名管道的文件虽然可见于文件系统,但是只是标识符,并非通信介质      
C正确 管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥      
D错误 多个进程只要能够访问同一管道就可以实现通信,不限于读写个数

6. 以下关于管道描述正确的有:

A.命名管道和匿名管道的区别在于命名管道是通过普通文件实现的
B.命名管道在磁盘空间足够的情况下可以持续写入数据
C.多个进程在通过管道通信时,删除管道文件则无法继续通信
D.命名管道的本质和匿名管道的本质相同都是内核中的一块缓冲区

答案:D
A错误,管道的本质是内核中的缓冲区,命名管道文件是缓冲区的标识
B错误,管道在缓冲区写满后会写阻塞,跟磁盘空间并无关系
C错误,管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信
D正确

7. 以下关于管道描述正确的有(2):

A.命名管道可以用于同一主机上的任意进程间通信
B.向命名管道中写入的数据越多,则管道文件越大
C.若以只读的方式打开命名管道时,则打开操作会报错
D.命名管道可以实现双向通信

答案:A

  • 匿名管道只能用于具有亲缘关系的进程间通信,命名管道可用于同一主机上的任意进程间通信
  • 管道的通信本质是通过内核中一块缓冲区(内存)时间数据传输,而命名管道的管道文件只是一个标识符,用于让多个进程能够访问同一块缓冲区
  • 管道是半双工通信,是可以选择方向的单向通信
  • 命名管道打开特性为,若以只读方式打开文件,则会阻塞,直到管道被以写的方式打开,反之亦然

根据以上理解分析:A选项正确,其他选项错误。

8. 以下描述正确的有 

A.共享内存实现通信的原理是因为所有进程操作映射同一块物理内存
B.共享内存的操作是进程安全的
C.共享内存被删除后,则其它进程直接无法实现通信
D.所有进程与共享内存断开映射后,则共享内存自动被释放

答案:A
A正确,共享内存的本质就是开辟一块物理内存,让多个进程映射同一块物理内存到自己的地址空间进行访问,实现数据共享的。
B错误,共享内存的操作是非进程安全的,多个进程同时对共享内存读写是有可能会造成数据的交叉写入或读取,造成数据混乱
C错误,共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
D错误,共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统(当然这里指的是非手动操作,可以手动删除)

9. 进程间通讯的方式中哪种的访问速度最快?

A.管道
B.消息队列
C.共享内存
D.套接字

答案:C
共享内存是将同一块物理内存映射到各个进程虚拟地址空间,可以直接通过虚拟地址访问,相较于其它方式少了两步内核态与用户态之间的数据拷贝因此速度最快

10.  以下描述正确的有

A.使用ipcrm -m命令删除指定共享内存后,则会直接释放共享内存
B.使用ipcs -m命令删除指定共享内存后,则会直接释放共享内存
C.使用ipcrm -a选项可以删除所有进程间通信资源
D.使用ipcrm命令不指定选项可以删除所有进程间通信资源

答案:C
A/B 共享内存只有在当前映射连接数为0时才会被删除释放

11. 以下关于ipcrm命令描述正确的有

A.ipcrm命令不指定选项可以删除所有进程间通信
B.ipcrm -m命令可以删除共享内存
C.ipcrm -s命令可以删除共享内存
D.ipcrm -q命令可以删除管道

答案:B
ipcrm 删除进程间通信资源
- m 针对共享内存的操作
- q 针对消息队列的操作
- s 针对信号量的操作
- a 针对所有资源的操作

根据以上理解分析:
A错误,ipcrm - a 选项是针对所有进程间通信资源
B正确
C错误, - s针对的是信号量
D错误, - q针对的是消息队列

12. 以下关于ipc命令描述正确的有:

A.ipcs -m用于查看消息队列的信息
B.ipcs -q可以查看消息队列的信息
C.ipcrm -s可以查看共享内存的信息
D.ipcrm -q可以查看共享内存的信息

答案:B
ipcs 查看进程间通信资源 / ipcrm 删除进程间通信资源
- m 针对共享内存的操作
- q 针对消息队列的操作
- s 针对信号量的操作
- a 针对所有资源的操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值