3 多进程

理论基础

引入目的

  1. 实现多任务并发执行
  2. 数据在多个进程之间通信

基本概念

  • 进程是程序的一次执行过程,有一定的生命周期:创建态,就绪态,执行态,挂起态,死亡态
  • 系统中允许的每个进程都是拷贝父进程资源得到的
  • 进程退出后,分配的系统资源应该由父进程回收,否则会造成浪费资源
  • 进程是计算机资源分配的基本单位,系统会给每个进程分配4G的虚拟内存
  • 进程调度机制:时间片轮询上下文切换机制
  • 并行和并发区别:
    • 并发:单核CPU系统在处理多个任务时,使用相关的调度机制实现多个任务进行细化时间片轮询,在宏观上感觉是多个任务同时执行的操作,但同一时刻只有一个任务在被CPU处理
    • 并行:多核CPU处理多个任务时,同一时间,每个CPU处理的任务之间是并行的,真正意义上多个任务同时执行

进程的内存管理

  • 启动一个进程时,操作系统通过mmu(内存管理单元)将物理内存映射到虚拟内存
    • 虚拟内存:
      • 3-4G:内核空间,多个进程共享一个内核空间
      • 0-3G:用户空间,不同进程的用户空间相互独立
        • 动态区:程序运行时自动产生,该区未初始化的变量默认随机值
          • 栈区:局部变量,可读可写,从函数进入到函数return,由编译器管理
          • 堆区:malloc和free,可读可写,由程序员管理
        • 静态区:程序生成后就有
          • 数据区:全局变量和静态变量(static),可读可写,从程序执行到程序退出,常驻内存,该区未初始化的变量默认是0
          • rodata(read only data)区:常量,只读数,从程序执行到程序退出,常驻内存
          • 代码区:函数实现,只读,从程序执行到程序退出,常驻内存
  • swap分区:硬盘上的一个分区,内存空间不够用时,会将长时间不用的进程先放入交换分区,物理内存够用时再切换回去

进程的种类

  1. 交互进程:由Shell控制,可以直接和用户交互,如文本编辑器
  2. 批处理进程:内部维护一个队列,队列中的进程会被统一处理,如g++一步到位的编译
  3. 守护进程:脱离了终端,随系统启动而运行,随系统退出而停止,如系统的服务进程

特殊进程

  • 0进程(idel):空闲进程,系统启动后运行的第一个进程,没有其他进程运行时会运行该进程,是1号和2号的父进程
  • 1进程(init):由0号进程创建,完成一些硬件的必要的初始化工作,收养孤儿进程
  • 2进程(kthreadd)进程:调度进程,由0号进程创建,完成任务调度问题
  • 孤儿进程:当前进程还在运行,父进程就退出了
  • 僵尸进程:当前进程已退出,但父进程没有为其回收资源

进程状态

可以通过ps -aux查看进程状态

  • 主状态:
    • D:不可中断的休眠状态,通常是IO操作
    • R:运行态
    • S:可中断的休眠状态
    • T:停止态
    • t:调试时的停止
    • X:死亡态
    • Z:僵尸进程
    • I:空闲
  • 附加态:
    • <:高优先级,对其他用户不友好
    • N:低优先级,对其他用户友好
    • L:锁在内存中,不能被放到swap分区中
    • s:会话组的先导进程
    • l:多线程的进程
    • +:前台运行的进程,输入输出都占用终端

进程状态切换

  • jobs -l:查看当前终端所有停止的进程的作业号和进程号
  • bg 作业号:将暂停的作业在后台运行
  • fg 作业号:将后台运行的作业切换到前台运行

进程五态

创建态,就绪态,执行态,挂起态,死亡态

![[五态转换.jpg]]

多进程实现

新的进程直接拷贝父进程的资源,只改变少部分的数据,保留了父进程大部分数据,拷贝过程通过fork实现

创建进程

  • pid_t fork(void);:拷贝父进程得到子进程
    • 头文件:
      • <sys/types.h>
      • <unistd.h>
    • 返回值:创建成功,子进程得到0,父进程得到子进程的进程号;创建失败返回-1并置位错误码
  • 父子进程执行没有固定先后顺序,与抢占时间片先后有关
  • 若不管返回值,有\(n\)个fork,就有\(2^n\)个进程;若在子进程代码块中再创建进程,就是子进程再创建子进程
pid_t pid=fork();    //从fork行开始后面的代码父子进程一样
if(pid>0){    //父进程
	
	wait(nullptr);
}
else if(pid==0) {    //子进程
	
	exit(EXIT_SUCCESS);
}
else {    //创建失败
	perror("fork error");
	return -1;
}

替换进程

  • int execl(const char *path, const char *arg, .../* (char *) NULL */);:用另外一个程序重构用户空间
    • 头文件:<unistd.h>
    • 参数:
      • 用于替换的可执行文件的文件名
      • 要传给程序的参数:arg0,arg1,arg2...,以nullptr作为结束标志
    • 返回值:失败返回-1并置位错误码
pid_t pid=fork();    //从fork行开始后面的代码父子进程一样
if(pid>0){    //父进程
	
	wait(nullptr);
}
else if(pid==0) {    //子进程
	if(execl(文件名,参数...)==-1){    //替换子进程
		perror("execl error");
		return -1;
	}
}
else {    //创建失败
	perror("fork error");
	return -1;
}

获取进程号

  • pid_t getpid(void);:获取当前进程的进程号
    • 头文件:
      • <sys/types.h>
      • <unistd.h>
    • 返回值:当前进程的进程号
  • pid_t getppid(void);:获取父进程的进程号
    • 头文件:
      • <sys/types.h>
      • <unistd.h>
    • 返回值:父进程进程的进程号

退出进程

  • void exit(int status);:退出当前进程,库函数,会刷新并关闭标准IO的缓冲区
    • 头文件:<stdlib.h>
      • 参数:退出时的状态,会和0377做与运算后返回给父进程
        • EXIT_SUCCESS:返回成功,即0
        • EXIT_FAILURE:返回失败,即1
  • void _exit(int status);:退出当前进程,内核函数,不会会刷新并关闭标准IO的缓冲区
    • 头文件:<unistd.h>
    • 参数:退出时的状态,会和0377做与运算后返回给父进程
      • EXIT_SUCCESS:返回成功,即0
      • EXIT_FAILURE:返回失败,即1

回收进程资源

  • pid_t wait(int *wstatus);:阻塞回收子进程的资源,等价于waitpid(-1,wstatus,0)
    • 头文件:
      • <sys/types.h>
      • <sys/wait.h>
    • 参数:子进程退出时,获取子进程退出时的状态与0377做与运算后的结果,可以传nullptr
    • 返回值:成功返回回收进程的进程号,失败返回-1并置位错误码
  • pid_t waitpid(pid_t pid, int *wstatus, int options);:阻塞或非阻塞回收指定进程的资源
    • 头文件:
      • <sys/types.h>
      • <sys/wait.h>
    • 参数:
      • 进程号
        • \(\gt 0\):回收指定进程号的进程
        • \(= 0\):回收当前相同进程组的子进程
        • \(= -1\):回收任意子进程
        • \(\lt -1\):回收指定进程组中的子进程,进程组号就是参数的绝对值
      • 退出状态:子进程退出时,获取子进程退出时的状态与0377做与运算后的结果,可以传nullptr
      • 是否阻塞:
        • 0:阻塞回收
        • WNOHANG:非阻塞回收
    • 返回值:
      • \(\gt 0\):回收到的子进程的进程号
      • \(= 0\):没有回收到子进程
      • \(= -1\):回收失败,置位错误码

进程间通信

多个进程间的用户空间相互独立,无法通过用户区完成通信
可以通过内核空间通信

  • IPC:interprocess communication,进程间通信
  • 使用内核空间实现多个进程间通信,根据使用的容器或方式不同,分类:
    1. 内核提供的通信方式(传统):
      1. 无名管道
      2. 有名管道
      3. 信号
    2. system V提供的通信方式:
      1. 消息队列
      2. 共享内存
      3. 信号量(信号灯集)
    3. 套接字通信:Socket网络通信(跨主机通信)

管道

  • 管道是一种特殊的文件,不用于存储数据,只用于进程间通信
  • 管道分为有名管道和无名管道
  • 在内核空间中创建一个管道通信,一个进程可以将数据写入管道,经由管道缓冲到另一个进程读取

管道通信特点:

  • 管道可以实现自己和自己通信
  • 对管道中数据的操作是一次性的,管道中的数据被读取后就从管道中消失了
  • 管道文件的大小:64K
  • 对管道的操作只能使用文件IO方式,但不可以lseek偏移文件光标,必须先进后出(先写后读)
  • 读写特点:
    • 读端存在时:写端写满64k后再写入会阻塞
    • 当读端不存在时:写端写满后再向管道中写入数据时,会发生管道破裂,内核空间会向用户空间发送SIGPIPE信号,进程收到该信号后退出
    • 当写端存在时:管道中没有数据后再读取会阻塞
    • 当写端不存在时:管道中没有数据后再读取不会会阻塞
  • 管道通信是半双工通信方式,两个管道可以实现全双工
    • 单工:只能A向B发信息
    • 半双工:同一时刻只能A向B发信息
    • 全双工:任意时刻,A和B能相互发消息

无名管道

  • 在内存中创建,不存在于文件系统,随进程结束而消失
  • 仅适用于亲缘进程间通信

API:

  • int pipe(int pipefd[2]);:创建一个无名管道
    • 头文件:<unistd.h>
    • 参数:整形数组,用于接收无名管道的两个文件描述符
      • pipefd[0]:读端
      • pipefd[1]:写端
    • 返回值:成功返回0,失败返回-1并置位错误码
    • 注意:
      • 需要在创建子进程前创建管道才能使子进程和父进程通信
      • 使用完文件描述符需要将所有读端和所有写端的文件描述符关掉,无名管道才会消失

实例

#include <myHead.h>

using namespace std;

int main(int argc,char* argv[]){
    int fds[2];
    if(pipe(fds)==-1){    //创建管道
        perror("create pipe error");
        return -1;
    }
    cout<<fds[0]<<" "<<fds[1]<<endl;
    pid_t pid=fork();
    if(pid>0){    //父进程
        close(fds[0]);
        char buff[128]="hello world";
        write(fds[1],buff,strlen(buff));
        close(fds[1]);
        wait(nullptr);
    }
    else if(pid==0){    //子进程
        close(fds[1]);
        char buff[128]="";
        read(fds[0],buff,sizeof(buff)-1);
        cout<<buff<<endl;
        close(fds[0]);
        exit(EXIT_SUCCESS);
    }
    else {
        perror("fork error");
        return -1;
    }
    return 0;
}

有名管道

  • 会在文件系统中创建一个管道文件
  • 可以实现任意进程间通信

API:

  • int mkfifo(const char *pathname, mode_t mode);:创建一个管道文件
    • 头文件:
      • <sys/types.h>
      • <sys/stat.h>
    • 参数:
      • 管道文件名
      • 管道文件权限,最终新文件的权限为mode&(~umask)
    • 返回值:成功返回0,失败返回-1并置位错误码
    • 注意:管道文件创建后,其他进程可以对管道文件打开读写,必须要保证管道文件的两端都没有关闭才能执行读写操作,否则会在open阻塞

实例

#include <myHead.h>

using namespace std;

int main(int argc,const char* argv[]){
    if(mkfifo("./myfifo",0644)==-1){
        perror("mkfifo errir");
        return -1;
    }
    cout<<"create fifo successfully"<<endl;
    return 0;
}
#include <myHead.h>

using namespace std;

int main(int argc,const char* argv[]){
    int sfd=-1;
    if((sfd=open("./myfifo",O_WRONLY))==-1){
        perror("send open error");
        return -1;
    }
    char wbuf[128]="";
    while(true){
        fputs("输入:\n",stdout);
        fgets(wbuf,sizeof(wbuf)-1,stdin);
        wbuf[strlen(wbuf)-1]='\0';
        write(sfd,wbuf,strlen(wbuf));

        if(strcmp(wbuf,"quit")==0)break;
    }
    close(sfd);
    return 0;
}
#include <myHead.h>

using namespace std;

int main(int argc,const char* argv[]){
    int rfd=-1;
    if((rfd=open("./myfifo",O_RDONLY))==-1){
        perror("send open error");
        return -1;
    }
    char rbuf[128]="";
    while(true){
        memset(rbuf,0,sizeof(rbuf));
        read(rfd,rbuf,sizeof(rbuf)-1);
        fprintf(stdout,"received:%s\n",rbuf);
        if(strcmp(rbuf,"quit")==0)break;
    }
    close(rfd);
    return 0;
}

信号

信号只能实现多个进程间通知作用,不能实现数据传输

  • 信号是软件模拟硬件的中断功能,信号由软件实现,中断由硬件实现
    • 中断:停止当前正在执行的事情去做另一件事情
  • 用户,内核,进程都可以给进程发送信号
    • 用户:如输入Ctrl+C
    • 内核:如管道破裂时发送SIGPIPE
    • 进程:通过相关函数实现
  • 信号通信属于异步通信
    • 同步:多个任务有执行先后顺序
    • 异步:多个任务没有执行先后顺序
  • 对信号的处理方式:捕获,忽略,默认
  • 执行信号处理函数期间,系统默认屏蔽该信号

绑定处理方式

  • sighandler_t signal(int signum, sighandler_t handler):将信号与信号处理方式绑定到一起
    • 头文件:<signal.h>
    • 参数:
      • 要处理的信号
        • SIGKILL和SIGSTOP是不合法的参数
      • 处理方式:
        • SIG_IGN:忽略
        • SIG_DFL:默认,一般默认都是杀死进程
        • typedef void (*sighandler_t)(int);函数指针,用户自定义,参数是要处理的信号
    • 返回值:成功返处理方式的起始地址,失败返回SIG_ERR并置位错误码
    • 注意:只要信号与信号处理方式绑定以后,收到信号后处理方式会立即响应
#include <myHead.h>

using namespace std;

int main(int argc,const char* argv[]){
    auto handler=[](int signum){
        if(signum==SIGCHLD){
	        //由于生产僵尸进程速度过快,需要循环非阻塞回收
            while(waitpid(-1,nullptr,WNOHANG)>0);
        }
    };
    if(signal(SIGCHLD,handler)==SIG_ERR){
        perror("signal error");
        return -1;
    }
    //创建多个僵尸进程
    for(int i=0;i<5;i++){
        if(fork()==0){    //创建子进程后立即退出子进程
            exit(EXIT_SUCCESS);
        }
    }
    while(true);
    return 0;
}
  • int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);:重新设置信号处理方式
    • 参数:
      • 信号,可以是SIGKILL和SIGSTOP
      • 新的处理方式
      • 用于保存旧的处理方式,可以为空
    • 返回值:成功返回0,失败返回-1并置位错误码
    • sigaction结构体:
    struct sigaction {
       void(*sa_handler)(int);    //信号处理函数
       void(*sa_sigaction)(int, siginfo_t *, void *);    //信号处理函数,可以获得信号的详细信息
       sigset_t   sa_mask;    //指定处理信号期间需要屏蔽的信号
       int        sa_flags;    //指定信号处理行为
       void     (*sa_restorer)(void);    //一般不用
    };
    
    • sa_flags:
      • SA_RESTART,使被信号打断的系统调用自动重新发起
      • SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
      • SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
      • SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
      • SA_RESETHAND,信号处理之后重新设置为默认的处理方式
      • SA_SIGINFO,使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
  • int sigfillset(sigset_t *set);:用所有信号初始化set

发送信号

  • int kill(pid_t pid, int sig);:向指定进程或进程组发送信息
    • 头文件:<signal.h>
    • 参数:
      • 进程号或进程组号
        • \(\gt 0\):向指定进程发信号
        • \(=0\):向同进程组中的所有进程发信号
        • \(=-1\):向所有进程发信号
        • \(\lt -1\):向指定进程组发信号,进程组id为参数的绝对值
      • 要发送的信号
    • 返回值:成功返回0,失败返回-1并置位错误码
  • int raise(int sig);:向自己发送信号,等价于kill(getpid(),sig)
    • 头文件:<signal.h>
    • 参数:要发送的信号
    • 返回值:成功返回0,失败返回非0值
  • unsigned int alarm(unsigned int seconds);:在指定时间后向调用者进程发送SIGALRM信号
    • 参数:指定秒数,若为0表示取消之前设置的所有alarm
    • 返回值:成功时返回先前设置的alarm剩余的时间,若先前没有alarm返回0

实例

#include <myHead.h>

using namespace std;

int main(int argc,const char* argv[]){
    auto handler=[](int signum){
        if(signum==SIGUSR1){
            wait(nullptr);
            cout<<"son of a bitch"<<endl;
            raise(SIGKILL);
        }
    };
    if(signal(SIGUSR1,handler)==SIG_ERR){    //绑定信号和处理方式
        perror("signal error");
        return -1;
    }
    pid_t pid=fork();
    if(pid>0){    //父进程
        while(true){
            cout<<"mofei daisuki"<<endl;
            sleep(1);
        }
    }
    else if(pid==0){    //子进程
        sleep(5);
        cout<<"die with me"<<endl;
        kill(getppid(),SIGUSR1);    //向父进程发送信号
        exit(EXIT_SUCCESS);
    }
    else {
        perror("fork error");
        return -1;
    }
    return 0;
}

IPC对象

  • 消息队列,共享内存,信号量都是借助内核空间完成的
  • 在内核空间中创建出相关的对象容器,进行进程间通信时,将信息放入对象容器中,另一个进程从容器中取数据
  • system V的IPC对象实现了数据传输的容器和程序的分离,程序结束后容器中的数据依然存在,除非手动删除

system V提供的进程间通信IPC对象相关指令:

  • ipcs [选项]:查看所有信息
    • -q:查看消息队列信息
    • -m:查看共享内存信息
    • -s:查看信号量信息
  • ipcrm [选项] id:删除指定IPC对象
    • -q:删除消息队列信息
    • -m:删除共享内存信息
    • -s:删除信号量信息

创建key值

  • key_t ftok(const char *pathname, int proj_id);:通过给定文件路径和随机数创建一个4字节整数的key值,用于system V的IPC对象的创建
    • 头文件:
      • <sys/types.h>
      • <sys/ipc.h>
    • 参数:key值由参数组成
      • 一个已存在的文件路径,提供key值3字节的内容
        • 文件设备号占1字节
        • 文件incode号占2字节
      • 一个非零随机整数,取后8位,占key值的1字节
    • 返回值:成功返回key值,失败返回-1并置位错误码

消息队列

  • 对消息队列中的消息的读取是一次性的
  • 消息队列大小:16K

创建&打开

  • int msgget(key_t key, int msgflg):通过给定的key值创建/打开消息队列并返回消息队列id
    • 头文件:
      • <sys/types.h>
      • <sys/ipc.h>
      • <sys/msg.h>
    • 参数:
      • key值:
        • IPC_PRIVATE:只用于亲缘进程间通信
        • ftok创建出来的key值,可以用于任意进程间通信
      • 创建标识:多个标识用|隔开
        • IPC_CREAT:创建并打开一个消息队列,若消息队列已存在,直接打开
        • IPC_EXCL:确保要创建一个消息队列,若消息队列已存在,报错,错误码为EEXIST
        • 权限:消息队列操作的权限,如0644
    • 返回值:成功返回消息队列id,失败返回-1并置位错误码

发送消息

发送信息需要对消息队列有写权限

  • int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);:向消息队列存放一个消息
    • 头文件:
      • <sys/types.h>
      • <sys/ipc.h>
      • <sys/msg.h>
    • 参数:
      • 消息队列id
      • 要发送的消息的起始地址,一般定义为一个结构体
        struct mymsg {
           long   mtype;    //消息类型,只能为正整数
           //后面是消息正文
        };
        
      • 消息结构体中正文的大小
      • 标识是否阻塞
        • 0:阻塞形式向消息队列中存放信息,消息队列满了会在该函数处阻塞
        • IPC_NOWAIT:非阻塞形式向消息队列中存放信息,消息队列满了直接返回
    • 返回值:成功返回0,失败返回-1并置位错误码

接收消息

接收消息需要对消息队列有读权限

  • ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);:从消息队列中取出一个消息
    • 头文件:<sys/msg.h>
    • 参数:
      • 消息队列id
      • 接收消息的起始地址,一般定义为一个结构体
      • 消息结构体中正文的大小
      • 要接收的消息的类型
        • \(=0\):读取消息队列中队首的消息,无论消息类型
        • \(\gt 0\):读取消息队列中第一个msgtyp类型的消息
        • \(\lt 0\):读取消息队列中第一个绝对值小于msgtyp的类型
      • 标识是否阻塞
        • 0:阻塞形式从消息队列中读取消息,消息队列空了会在该函数处阻塞
        • IPC_NOWAIT:非阻塞形式从消息队列中读取信息,消息队列空了直接返回
    • 返回值:成功读取返回实际读取的正文大小,失败返回-1并置位错误码

控制

  • int msgctl(int msqid, int cmd, struct msqid_ds *buf);:对指定的消息队列执行相关操作,具体由cmd参数决定
    • 头文件:
      • <sys/types.h>
      • <sys/ipc.h>
      • <sys/msg.h>
    • 参数:
      • 消息队列id

      • 要执行的操作

        • IPC_STAT:获取当前消息队列的属性,参数3用于存放消息队列的属性
        • IPC_SET:设置当前消息队列的属性,参数3是要设置的消息队列的属性的结构体起始地址
        • IPC_RMID:销毁消息队列
      • 消息队列属性结构体地址:若cmd为IPC_RMID,传nullptr

        struct msqid_ds {
           struct ipc_perm msg_perm;    //消息队列所有者和权限
           time_t          msg_stime;    //最后一次发送消息时间
           time_t          msg_rtime;    //最后一次接收消息时间
           time_t          msg_ctime;    //最后一次状态更改时间
           unsigned long   __msg_cbytes;    //已用字节数
           msgqnum_t       msg_qnum;    //消息个数
           msglen_t        msg_qbytes;    //最大消息队列大小
           pid_t           msg_lspid;    //最后一次发送消息的进程号
           pid_t           msg_lrpid;    //最后一次接收消息的进程号
        };
        
        struct ipc_perm {
           key_t          __key;    //key值
           uid_t          uid;    //当前进程的uid
           gid_t          gid;    //当前进程的gid
           uid_t          cuid;    //消息队列创建者的uid
           gid_t          cgid;    //消息队列创建者的gid
           unsigned short mode;    //消息队列的权限
           unsigned short __seq;    //队列号
        };
        
    • 返回值:成功返回0,失败返回-1并置位错误码

实例

#include <myHead.h>

using namespace std;

struct msgbuf_{
    long mtype;
    char msg[1024];
};

int main(int argc,const char* argv[]){
    //创建key值
    key_t key=ftok("/",'m');
    if(key==-1){
        perror("ftok error");
        return -1;
    }
    printf("key=%x\n",key);
    
    //创建,打开消息队列
    int mqid=msgget(key,IPC_CREAT|0644);
    if(mqid==-1){
        perror("msgget error");
        return -1;
    }
    cout<<"mqid="<<mqid<<endl;

    //发送消息
    msgbuf_ buff;
    const size_t msgsize=sizeof(buff.msg);
    while(true){
        printf("input message type:");
        scanf("%ld", &buff.mtype);
        getchar();
        printf("input message text:");
        fgets(buff.msg, msgsize, stdin);
        buff.msg[strlen(buff.msg)-1] = 0;
        if(msgsnd(mqid, &buff, msgsize, 0)==-1){
            perror("msgsnd error");
            return -1;
        }
        if(strcmp(buff.msg,"quit")==0){
            break;
        }
    }
    return 0;
}
#include <myHead.h>

using namespace std;

struct msgbuf_{
    long mtype;
    char msg[1024];
};

int main(int argc,const char* argv[]){
    //创建key值
    key_t key=ftok("/",'m');
    if(key==-1){
        perror("ftok error");
        return -1;
    }
    printf("key=%x\n",key);
    
    //创建,打开消息队列
    int mqid=msgget(key,IPC_CREAT|0644);
    if(mqid==-1){
        perror("msgget error");
        return -1;
    }
    cout<<"mqid="<<mqid<<endl;

    //接收消息
    msgbuf_ buff;
    const size_t msgsize=sizeof(buff.msg);
    while(true){
        memset(&buff,0,sizeof(buff));
        if(msgrcv(mqid,&buff,msgsize,0,0)==-1){
            perror("msgrcv error");
            return -1;
        }
        printf("received:\t%s\n",buff.msg);
        if(strcmp(buff.msg,"quit")==0)break;
    }
    //销毁消息队列
    if(msgctl(mqid,IPC_RMID,nullptr)==-1){
        perror("msgctl error");
        return -1;
    }
    return 0;
}

共享内存

  • 通过一段物理内存在内核区中映射出一个共享内存段,不同进程通过共享内存段的起始地址向共享内存段读写数据
  • 使用时可能产生竞态,为了解决这个问题,共享内存一般会跟信号量一起使用,完成进程的同步功能
  • 消息队列保证数据不丢失,共享内存保证数据的时效性
  • 对共享内存的数据的读取不是一次性的
  • 使用共享内存时,不需要从用户空间切换到内核空间,是所有进程间通信方式中效率最高的一种通信方式

创建&打开

  • int shmget(key_t key, size_t size, int shmflg);:申请指定大小的物理内存,映射到内核空间,创建出共享内存段
    • 头文件:
      • <sys/ipc.h>
      • <sys/shm.h>
    • 参数:
      • key值:
        • IPC_PRIVATE:只用于亲缘进程间通信
        • ftok创建出来的key值,可以用于任意进程间通信
      • 申请的共享内存段的大小,是PAGE_SIZE即一页(4026字节)的整数倍,向上取整
      • 创建标识:多个标识用|隔开
        • IPC_CREAT:创建并打开一个消息队列,若消息队列已存在,直接打开
        • IPC_EXCL:确保要创建一个消息队列,若消息队列已存在,报错,错误码为EEXIST
        • 权限:共享内存操作的权限,如0644
    • 返回值:成功返回共享内存id,失败返回-1并置位错误码

映射

  • void *shmat(int shmid, const void *shmaddr, int shmflg);:将共享内存段映射到用户空间
    • 头文件:
      • <sys/types.h>
      • <sys/shm.h>
    • 参数:
      • 共享内存id
      • 物理内存的起始地址,一般传nullptr,由系统自动选择一个合适的对齐页
      • 对共享内存的操作:
        • 0:可读可写
        • SHM_RDNOLY:只读
    • 返回值:成功返回共享内存的起始地址,失败返回(void*)-1并置位错误码

释放映射关系

  • int shmdt(const void *shmaddr);:释放进程于共享内存的映射关系
    • 头文件:
      • <sys/types.h>
      • <sys/shm.h>
    • 参数:共享内存的起始地址
    • 返回值:成功返回0,失败返回-1并置位错误码

控制

  • int shmctl(int shmid, int cmd, struct shmid_ds *buf);:对指定的共享内存执行相关操作,具体由cmd参数决定
    • 头文件:
      • <sys/ipc.h>
      • <sys/shm.h>
    • 参数:
      • 共享内存段id

      • 要执行的操作

        • IPC_STAT:获取当前共享内存的属性,参数3用于存放共享内存的属性
        • IPC_SET:设置当前共享内存的属性,参数3是要设置的共享内存的属性的结构体起始地址
        • IPC_RMID:销毁共享内存
      • 共享内存属性结构体地址:若cmd为IPC_RMID,传nullptr

        struct shmid_ds {
           struct ipc_perm shm_perm;    /* Ownership and permissions */
           size_t          shm_segsz;   /* Size of segment (bytes) */
           time_t          shm_atime;   /* Last attach time */
           time_t          shm_dtime;   /* Last detach time */
           time_t          shm_ctime;   /* Last change time */
           pid_t           shm_cpid;    /* PID of creator */
           pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
           shmatt_t        shm_nattch;  /* No. of current attaches */
           ...
        };
        
    • 返回值:成功返回0,失败返回-1并置位错误码

实例

#include <myHead.h>

using namespace std;

#define PAGE_SIZE 4096

int main(int argc,const char* argv[]){
    //创建key值
    key_t key=ftok("/",'m');
    if(key==-1){
        perror("ftok error");
        return -1;
    }
    printf("key=%x\n",key);

    //创建,打开共享内存
    int size=PAGE_SIZE;
    int shmid=shmget(key,size,IPC_CREAT|0644);
    if(shmid==-1){
        perror("shmget error");
        return -1;
    }

    //映射
    char* addr=(char*)shmat(shmid,nullptr,0);
    if(addr==(void*)-1){
        perror("shmat error");
        return -1;
    }
    printf("addr=%p\n",addr);

    //写入
    while(true){
        fputs("input:\n",stdout);
        fgets(addr,size,stdin);
        addr[strlen(addr)-1]=0;
        if(strcmp(addr,"quit")==0){
            break;
        }
    }

    //取消映射
    if(shmdt(addr)==-1){
        perror("shmdt error");
        return -1;
    }
    return 0;
}
#include <myHead.h>

using namespace std;

#define PAGE_SIZE 4096

int main(int argc,const char* argv[]){
    //创建key值
    key_t key=ftok("/",'m');
    if(key==-1){
        perror("ftok error");
        return -1;
    }
    printf("key=%x\n",key);

    //创建共享内存
    int size=PAGE_SIZE;
    int shmid=shmget(key,size,IPC_CREAT|0644);
    if(shmid==-1){
        perror("shmget error");
        return -1;
    }

    //映射
    char* addr=(char*)shmat(shmid,nullptr,0);
    if((void*)addr==(void*)-1){
        perror("shmat error");
        return -1;
    }
    printf("addr=%p\n",addr);

    //读取
    while(true){
        sleep(1);
        printf("received:  %s\n",addr);
        if(strcmp(addr,"quit")==0){
            break;
        }
    }

    //取消映射
    if(shmdt(addr)==-1){
        perror("shmdt error");
        return -1;
    }

    //销毁共享内存段
    if(shmctl(shmid,IPC_RMID,nullptr)==-1){
        perror("shmctl error");
        return -1;
    }
    return 0;
}

信号量(信号灯集)

  • 本质上维护一个信号量数组,信号量是一个整数
  • 一个进程向一个信号量申请资源
    • 若信号量资源大于0,进程获得资源,执行进程后面的语句,对应的信号量减小
    • 若信号量资源等于0,进程可以阻塞等待,直到获得资源
  • 一个进程释放资源到一个信号量,这个信号量增加
  • 信号量实现多个进程间同步问题,一般不进行通信

创建&打开

  • int semget(key_t key, int nsems, int semflg);:创建信号灯集
    • 头文件:
      • <sys/types.h>
      • <sys/ipc.h>
      • <sys/sem.h>
    • 参数:
      • key值
      • 信号量个数
      • 创建标识:多个标识用|隔开
        • IPC_CREAT:创建并打开一个信号灯集,若信号灯集已存在,直接打开
        • IPC_EXCL:确保要创建一个信号灯集,若信号灯集已存在,报错,错误码为EEXIST
        • 权限:信号灯集操作的权限,如0644
    • 返回值:成功返回信号灯集id,失败返回-1并置位错误码

操作

  • int semop(int semid, struct sembuf *sops, size_t nsops);:P(申请资源)V(释放资源)操作
    • 头文件:
      • <sys/types.h>
      • <sys/ipc.h>
      • <sys/sem.h>
    • 参数:
      • 信号灯集id
      • 有关操作的结构体
        struct sembuf{
           unsigned short sem_num;    //要操作的信号量编号,即下标
           short          sem_op;    //要进行的操作,大于0释放资源,小于0申请资源
           short          sem_flg;    //操作标识,0阻塞,IPC_NOWAIT非阻塞
        }
        
      • 要操作的信号量个数
    • 返回值:成功返回0,失败返回-1并置位错误码

控制

  • int semctl(int semid, int semnum, int cmd, ...);:对指定的信号灯集执行相关操作,具体由cmd参数决定
    • 头文件:
      • <sys/types.h>
      • <sys/ipc.h>
      • <sys/sem.h>
    • 参数:
      • 信号灯集id
      • 要控制的信号量编号(下标),编号从0开始
      • 要执行的操作
        • IPC_RMID:销毁信号灯集,参数2忽略,参数4可以不传
        • SETVAL:对参数2对应的信号量设置值
        • GETVAL:对参数2对应的信号量获取值
        • SETALL:对所有信号量设置值
        • GETALL:对所有信号量获取值
        • IPC_STAT:获取当前信号灯集的属性
        • IPC_SET:设置当前信号灯集的属性
      • 是一个共用体,根据不同的参数3,传入不同的值
        union semun {
           int              val;    //SETVAL要设置的值
           struct semid_ds *buf;     //对于IPC_STAT,IPC_SET
           unsigned short  *array;    //对于GETALL,SETALL
           struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */
        };
        
    • 返回值:成功时,GETVAL返回对应值,其他返回0;失败返回-1并置位错误码

封装API

封装为信号灯集的创建,销毁,申请资源,释放资源

#pragma once
#include <myHead.h>

union semun {
    int              val;    //SETVAL要设置的值
    struct semid_ds* buf;     //对于IPC_STAT,IPC_SET
    unsigned short* array;    //对于GETALL,SETALL
    struct seminfo* __buf;  /* Buffer for IPC_INFO(Linux-specific) */
};

class Sem {
public:
    bool setVal(int index);
    Sem(key_t key, int count);
    bool P(int index);    //申请资源
    bool V(int index);    //释放资源
    bool deleteSem();    //销毁信号灯集

private:
    int semID;
    int semCount;   //信号灯个数
};
#include "Sem.h"

Sem::Sem(key_t key, int count) {
    semCount = count;
    //打开信号灯集
    semID = semget(key, count, IPC_CREAT | IPC_EXCL | 0644);
    if (semID == -1) {
        if (errno == EEXIST) {
            semID = semget(key, count, IPC_CREAT | 0644);    //若信号灯集已存在,直接打开,不需要初始化
            if (semID == -1) {
                perror("semget error");
            }
        }
    }
    //第一次打开时初始化
    else {
        for (int i = 0;i < semCount;i++) {
            setVal(i);
        }
    }
}

bool Sem::setVal(int index) {
    int val = -1;
    printf("set value of sem whose index is %d:  ", index);
    scanf("%d", &val);
    getchar();    //把换行符吃了
    semun su;
    su.val = val;
    if (semctl(semID, index, SETVAL, su) == -1) {
        perror("semctl error");
        return false;
    } else return true;
}

bool Sem::P(int index) {
    sembuf sb;
    sb.sem_num = index;
    sb.sem_op = -1;
    sb.sem_flg = 0;
    if (semop(semID, &sb, 1) == -1) {
        perror("semop error");
        return false;
    } else return true;
}

bool Sem::V(int index) {
    sembuf sb;
    sb.sem_num = index;
    sb.sem_op = 1;
    sb.sem_flg = 0;
    if (semop(semID, &sb, 1) == -1) {
        perror("semop error");
        return false;
    } else return true;
}

bool Sem::deleteSem() {
    if (semctl(semID, 0, IPC_RMID) == -1) {
        perror("delete error");
        return false;
    }
    return true;
}

实例

实现共享内存的同步

#include <myHead.h>
#include "Sem.h"

using namespace std;

#define PAGE_SIZE 4096

int main(int argc, const char* argv[]) {
    //创建信号灯集
    key_t key_sem = ftok("/", 'o');
    Sem s(key_sem, 2);

    //创建共享内存的key值
    key_t key = ftok("/", 'm');
    if (key == -1) {
        perror("ftok error");
        return -1;
    }
    printf("key=%x\n", key);

    //创建,打开共享内存
    int size = PAGE_SIZE;
    int shmid = shmget(key, size, IPC_CREAT | 0644);
    if (shmid == -1) {
        perror("shmget error");
        return -1;
    }

    //映射
    char* addr = (char*)shmat(shmid, nullptr, 0);
    if (addr == (void*)-1) {
        perror("shmat error");
        return -1;
    }
    printf("addr=%p\n", addr);

    //写入
    while (true) {
        //申请0号信号量
        s.P(0);

        fputs("input:\n", stdout);
        fgets(addr, size, stdin);
        addr[strlen(addr) - 1] = 0;

        //释放1号信号量
        s.V(1);

        if (strcmp(addr, "quit") == 0) {
            break;
        }
    }

    //取消映射
    if (shmdt(addr) == -1) {
        perror("shmdt error");
        return -1;
    }

    return 0;
}
#include <myHead.h>
#include "Sem.h"

using namespace std;

#define PAGE_SIZE 4096

int main(int argc, const char* argv[]) {
    //创建信号灯集
    key_t key_sem = ftok("/", 'o');
    Sem s(key_sem, 2);

    //创建key值
    key_t key = ftok("/", 'm');
    if (key == -1) {
        perror("ftok error");
        return -1;
    }
    printf("key=%x\n", key);

    //创建共享内存
    int size = PAGE_SIZE;
    int shmid = shmget(key, size, IPC_CREAT | 0644);
    if (shmid == -1) {
        perror("shmget error");
        return -1;
    }

    //映射
    char* addr = (char*)shmat(shmid, nullptr, 0);
    if ((void*)addr == (void*)-1) {
        perror("shmat error");
        return -1;
    }
    printf("addr=%p\n", addr);

    //读取
    while (true) {
        //申请1号信号量
        s.P(1);

        printf("received:  %s\n", addr);

        //释放0号信号量
        s.V(0);

        if (strcmp(addr, "quit") == 0) {
            break;
        }
    }

    //取消映射
    if (shmdt(addr) == -1) {
        perror("shmdt error");
        return -1;
    }

    //销毁信号灯集
    s.deleteSem();

    //销毁共享内存段
    if (shmctl(shmid, IPC_RMID, nullptr) == -1) {
        perror("shmctl error");
        return -1;
    }
    return 0;
}
posted @ 2026-10-07 16:16  mofei1116  阅读(5)  评论(0)    收藏  举报