Linux —— 进程间通信

目录

1.进程通信的目的

2.进程间通信的方法

2.1管道

2.2System V IPC

2.2POSIX IPC

3.进程间通信实例

3.1匿名管道

3.2匿名管道小项目

3.3命名管道

3.4命名管道的实例

3.5system V共享内存

3.6System V共享内存代码实例

4.System V信号量

5.操作系统如何维护IPC资源

1.进程通信的目的

        1.数据传输:一个进程需要将它的数据发送给另一个进程

        2.资源共享:多个进程之间共享同样的资源

        3.通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某件事情(例如子进程退出时要通知父进程)

        4.进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时指导它的状态改变

2.进程间通信的方法

通信的本质就是操作系统直接或间接给通信双方的进程提供"内存空间",要通信的进程必须看到一份公共的资源。这份公共资源是有操作系统的不同模块提供的(所以有多种通信的方法)。

2.1管道

管道分为匿名管道命名管道,这些管道是基于文件系统的文件。管道文件是一个内存级文件,它有属于自己的内核缓冲区,它并不需要从磁盘中读取数据

管道的特征有如下几点:

        1.管道的生命周期跟随创建此管道的进程

        2.管道可以让具有亲缘关系的进程相互通信(常用于父子通信)

        3.管道是面向字节流的

        4.半双工 -- 管道一般只用来做单向通信

        5.互斥与同步机制 -- 对共享资源进行保护的方案

2.2System V IPC

这种方案聚焦在本地通信,在现代这个万物互联的时代,这种方案是明显落后的。

        1.System V 消息队列

        2.System V 共享内存

        3.System V 信号量

2.2POSIX IPC

这种方案使得通信可以跨主机,是现在主流的通信方法。

        1.消息队列

        2.共享内存

        3.信号量

        3.互斥量

        4.条件变量

        5.读写锁

3.进程间通信实例

3.1匿名管道

管道是一个文件,它是由进程打开的,也就说明当某个进程打开管道文件时,就与管道建立了读写关系

87b4c34b43fe43bbadf3f8b6e20c51a0.png

如果在这个进程基础之上创建一个子进程,这个子进程就会拷贝父进程的东西(除了管道文件)。 

a9d5f6d618374498a1103a80a4d27ce6.png

 此时建立的管道文件是双向通信的,不符合管道通信的特点,我们手动关闭一些文件,即可得到父子进程间的单向通信。
8a5ce4cde4ce46c5944d423c1fb5db59.png

下面用一份很简单的代码来实现上述的通信过程:


#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cassert>

// 实现父进程向管道写入 "i am father"
// 子进程从管道中读取父进程发送的数据
int main()
{
    int fds[2] = {0};   // 用来保存管道读端、写端的文件描述符
    int n = pipe(fds);    // 创建管道文件的系统调用
    assert(n == 0);   // pipe返回0证明创建管道成功
    // 管道文件创建创建成功后,会将读、写端的文件描述符写入fds
    // 其中,fds[0]表写端,fds[1]表读端

    pid_t id = fork();  // 创建子进程
    assert(id >= 0);

    if(id == 0) // 子进程
    {
        close(fds[1]);  // 子进程关闭写端

        while(true)
        {
            char buffer[1024] = {0};
            int s = read(fds[0],buffer,sizeof(buffer)-1);   // 从管道读数据
            if(s > 0)
            {
                buffer[s] = 0;
                std::cout <<  buffer << std::endl;
            }
            else if(s == 0)
            {
                std::cout << "未从管道读到任何数据!" << std::endl;
            }
        }

        close(fds[0]);
        exit(0);
    }
    else  // 父进程
    {
        close(fds[0]);  // 父进程关闭读端

        while(true)
        {
            char buffer[1024] = {0};
            snprintf(buffer,sizeof(buffer),"i am father");
            write(fds[1],buffer,strlen(buffer));    //向管道写入信息
            sleep(1);   
        }
    }

    n = waitpid(id,nullptr,0);  // 阻塞等待子进程退出
    assert(n == id);

    close(fds[1]);
    return 0;
}

以上代码能够实现一个最基本的通信,但我们需要注意一些细节:

        1.程序中的pipe是一个系统调用,它接收一个指针类型的参数,其内部将会在内存开辟一个管道文件,并将读端、写端的文件描述符放入参数所指的空间。返回值为0则表示正常退出

        2.用来存放管道读写端的fds数组,fds[0]表管道的读端文件描述符,fds[1]表管道的写端文件描述符

        3.为了更加严格的使用管道做单向通信,父进程需要关闭读端、子进程需要关闭写端

        4.如果管道的写端没有被关闭,且管道内没有任何数据,则读端将会阻塞,一直等待写端向管道发送数据(即程序会停留在read,不往下执行)

        5.如果管道的写端关闭,read函数将会返回0(读到0个数据)

       6.管道是一个固定大小的文件(其容量是有限的)

        7.如果管道的读端被关闭,那么写端存在就没有任何意义。此时write便会收到来自操作系统的终止信号

        8.如果读写端都没有被关闭,写端写数据的速度较快,而读端读数据的速度较慢,就会造成管道被写满,此时就会发生写端阻塞(即程序会停留在write,不往下执行)

        9.如果读写端都没有被关闭,写端写数据较慢,而读端读数据较快,就会造成管道没有任何数据,此时就会发生读端阻塞(见4)

3.2匿名管道小项目

上面的程序只实现了数据的传送。现在我们应该实现事件通知和进程控制。我们的想法如下:

        1.父进程创建N个子进程和N个管道

        2.父进程向某个管道发送"控制命令"

        3.子进程从对应的管道读取"控制命令"

        4.子进程根据"控制命令"做出相应的动作

7cf118cd9373435eb4da213ea8da686a.png

#include <unistd.h>
#include <iostream>
#include <cassert>
#include <string>
#include <cstdio>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAKE_RAND() srand((size_t)time(nullptr) ^ 15423 ^ 12)
///子进程执行的任务
void IO_mission()
{
    std::cout << getpid() << ":执行IO任务\n" << std::endl;
    sleep(1);
}

void flush_mission()
{
    std::cout << getpid() << ":执行刷新任务\n" << std::endl;
    sleep(1);
}

void download_mission()
{
    std::cout << getpid() << ":执行下载任务\n" << std::endl;
    sleep(1);
}

typedef void(*func) ();

void loadSubMission(std::vector<func>& vec_mis)   
{
    vec_mis.push_back(IO_mission);
    vec_mis.push_back(flush_mission);
    vec_mis.push_back(download_mission);
}

///
struct Information
{
    Information(int writeFd,int subId)
        :_writeFd(writeFd),_subId(subId)
    {
        char buffer[1024] = {0};
        snprintf(buffer,sizeof(buffer),"process%d[pid(%d)-writeFd(%d)]",_size++,_subId,_writeFd);
        _name = buffer; 
    }
    std::string _name;
    int _writeFd;   // 管道写端文件描述符
    pid_t _subId; // 子进程的pid
    static int _size;
};
int Information::_size = 1;

// 从管道读取信号
int readMisson(int fd)
{
    int sig = 0;
    int ret = read(fd,&sig,sizeof(int));
    if(ret > 0)
    {
        return sig;
    }
    else 
    {
        return -1;
    }
}

// 创建管道和子进程
void createSubprocess(int subSize,std::vector<Information>& vec_info,std::vector<func>& vec_mis)
{
    for(int i=0;i<subSize;i++)
    {
        int fds[2] = {0};
        int n = pipe(fds);
        assert(n == 0);

        pid_t id = fork();
        assert(id >= 0);

        if(id == 0) // 子进程
        {
            close(fds[1]);

            while(true)
            {
                int sig = readMisson(fds[0]);
                if(sig >= 0 && sig < vec_mis.size())
                {
                    vec_mis[sig]();
                }
                else if(sig == -1)
                {
                    break;
                }
            }

            close(fds[0]);
            exit(0);
        }

        close(fds[0]);

        // 保存每个管道的读端,每个子进程pid
        vec_info.push_back(Information(fds[1],id));
    }
}


// 向管道发送"控制信号"
void sendControlSig(std::vector<Information>& vec_info,std::vector<func>& vec_mis)
{
    int subSize = vec_info.size();  // 子进程数量
    int misSize = vec_mis.size();   // 任务数量
    
    int cnt = 4;
    while(cnt--)
    {
        // 随机选取管道、任务
        int subIndex = rand() % subSize;
        int misIndex = rand() % misSize;
        
        // 发送任务下标
        std::cout << misIndex << "号任务发送给" << vec_info[subIndex]._name << std:: endl;
        write(vec_info[subIndex]._writeFd,&misIndex,sizeof(int));
        sleep(1);
    }

    for(auto& e:vec_info)
    {
        close(e._writeFd);

        // 本来在这里可以直接等待子进程退出
        // 但是这么写会存在bug,非常严重的bug
        //waitpid(e._subId,nullptr,0);
    }
}


// 等待子进程退出
void waitSubExit(std::vector<Information>& vec_info)
{
    for(auto& e:vec_info)
    {
        int ret = waitpid(e._subId,nullptr,0);
        assert(ret == e._subId);

        std::cout << e._name << "已被回收" << std::endl;
    }
}


int main(int argc,char* argv[]) // 我们希望从命令行获取子进程数量
{
    MAKE_RAND();
    if(argc < 2)
    {
        std::cout << "命令行参数不够!" << std::endl;
        return -1;
    }
    int subSize = *(argv[1]) - '0';


    std::vector<Information> vec_Information;    // 存储管道、子进程信息的容器

    std::vector<func> vec_mission;  //存储子进程执行的任务的容器
    loadSubMission(vec_mission);    // 将子进程要执行的任务加载到容器中

    createSubprocess(subSize,vec_Information,vec_mission);
    sendControlSig(vec_Information,vec_mission);
    waitSubExit(vec_Information);
    return 0;
}

我们来研究以下程序注释中描述的重大bug。假设我们创建4个子进程,并且子进程会拷贝一份父进程的文件描述符表,那么子进程的文件描述符表可以是这样的:

5b929f9a4ae14f71bfdfa4782bd1d096.png

可以看到,除第一个子进程外,其余的子进程都拥有前面所有子进程所拥有的管道。所以当父进程关闭关闭第一个管道的写端时,第二个、第三个子进程依然占有第一个管道的写端,这就会导致read函数认为第一个管道的写端依然存在,继而发生写端阻塞。所以我们不能在代码中关闭完管道的写端后立马等待子进程退出。

解决方法一:将父进程关闭管道写端的过程和父进程回收子进程的过程各自封装成独立的函数。其用意在于:先批量化地将父进程地管道写端关闭,那么最后一个子进程的管道就没有写端,没有了写端操作系统就会发送一个终止信号,所以最后一个子进程退出,退出时,会将此子进程打开的所有文件关闭,也就是说,倒数第二个子进程的管道文件也没有了写端,依次类推。所以,先批量化的关闭父进程管道的写端后,每个子进程都处于僵尸状态,只需单独封装一个专门用来回收子进程的函数即可。

解决方法二:在子进程处理任务之前,就关掉所有管道的写端。这样做就能够实现一边关闭父进程管道的写端,也能立即回收管道对应的子进程。代码如下:

// 创建管道和子进程
void createSubprocess(int subSize,std::vector<Information>& vec_info,std::vector<func>& vec_mis)
{
    std::vector<int> deleteFd;  //保存管道写端的文件描述符
    for(int i=0;i<subSize;i++)
    {
        int fds[2] = {0};
        int n = pipe(fds);
        assert(n == 0);

        deleteFd.push_back(fds[1]); //保存写端

        pid_t id = fork();
        assert(id >= 0);

        if(id == 0) // 子进程
        {
            // 处理任务之前,子进程关掉所有管道写端
            for(auto& e:deleteFd)
            {
                close(e);
            }

            while(true)
            {
                int sig = readMisson(fds[0]);
                if(sig >= 0 && sig < vec_mis.size())
                {
                    vec_mis[sig]();
                }
                else if(sig == -1)
                {
                    break;
                }
            }

            close(fds[0]);
            exit(0);
        }

        close(fds[0]);

        // 保存每个管道的读端,每个子进程pid
        vec_info.push_back(Information(fds[1],id));

    }
}

3.3命名管道

命名管道与匿名管道一样,都是基于文件系统创建的。但是命名管道确实能够以文件的形式"躺"在磁盘上,是用户能看到的。又但是,根据匿名管道的特性,命名管道也不会向磁盘刷新任何数据,它也属于内存级文件。也就是说,管道通信的本质是通过内核的缓冲区

命名管道与匿名管道的最大的区别便是,命名管道允许两个毫无关系的进程进行通信。还有便是,命名管道的创建、打开规则不一样:

        1.匿名管道是通过系统调用pipe创建的,其参数是一种输出型参数。调用此函数的进程在函数调用完成后天然的打开了管道文件的读、写端。

        2.命名管道需要通过系统调用mkfifo创建,其参数列表如图:

a8bee9130f504b4cadfbc983743e3468.png通过mkfifo系统调用,能够在磁盘上创建一个具有指定名称的命名管道。与其他文件一样,在同一目录下,文件的名称是唯一的。

        3.命名管道需要被"删除"。这个删除不同于用户指令"rm",需要创建管道的进程使用unlink接口。

        4.命名管道创建好之后,还不能立即使用。两个想要通信的进程必须通过系统调用open打开管道文件。想要向管道写数据的进程,调用write即可;想要读数据的进程,调用read即可。

        5.当使用mkfifo创建好管道文件后,两个想要通信的进程,如果任意一方调用了open,任意一方没有调用open,此时调用了open的一方会发生"打开阻塞"。也就是说,两个进程想要通过命名管道通信,除了各自创建一次管道之外,还需要确保两个进程都通过open打开了命名管道

3.4命名管道的实例

创建一个名为server的进程和一个名为client的进程,server从管道读数据,client向管道写数据。因为两个想要通信的进程都需要单独创建一次管道文件,索性将创建管道的方法写成头文件:

// command.h
#pragma once

// 编写打开管道文件的方式
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <cstdio>
#include <cassert>

// 默认为本目录下的named_pipe文件
#define PIPE_NAME "named_pipe"

// 创建管道文件
bool createNamedPipe(const std::string& path)
{
    umask(0);   //使得权限是我们想要的
    int n = mkfifo(path.c_str(),0600);// 只允许拥有者读写
    if(n < 0)
    {
        // 返回值为-1则打开管道文件失败
        std::cout << "strerror:" << strerror(errno) << std::endl;
        return true;
    }
    else if(n == 0)
    {
        return true;
    }
}

// 关闭管道文件
bool closeNamedPipe(const std::string& path)
{
    int n = unlink(path.c_str()); // 关闭管道文件
    assert(n == 0);
    (void)n;
}
// server.cpp
#include "command.h"

// server端做读端

int main()
{
    bool r = createNamedPipe(PIPE_NAME);
    assert(r == true);
    (void)r;

    // 如果client端没有打开管道
    // 那么open将阻塞
    int rfd = open(PIPE_NAME,O_RDONLY);//只读方式打开

    if(rfd < 0)
    {
        exit(1);
    }

    char buffer[1024] = {0};
    while(true)
    {
        // 从管道中读取数据到buffer
        int n = read(rfd,buffer,sizeof(buffer)-1);
        if(n > 0)//read的返回值为读取到多少数据
        {
            buffer[n] = 0;
            std::cout << "client->server say #" << buffer << std::endl;
        }
        else if(n == 0)//如果数据为0,说明写端关闭
        {
            std::cout << "client quit, me too!" << std::endl;
            break;
        }
    }
    
    close(rfd);
    closeNamedPipe(PIPE_NAME);
    return 0;
}
// client.cpp
#include "command.h"

// client做写端

int main()
{

    // 如果server端没有打开管道文件
    // 那么open将阻塞
    int wfd = open(PIPE_NAME,O_WRONLY);//只写方式打开文件
    assert(wfd > 0);
    (void)wfd;

    char buffer[1024] = {0};
    while(true)
    {
        // 从stdin中读取数据到buffer里面来
        // 大小-1更加安全(虽然fgets会自动补\0)
        std::cout << "client say #";
        fgets(buffer,sizeof(buffer)-1,stdin);
        if(strlen(buffer) > 0)
        {
            // 事实上fgets一定会读到至少一个字符(\n)
            buffer[strlen(buffer)-1] = 0;//将读上来的回车吃掉
        }

        // 向管道中写入信息
        int n = write(wfd,buffer,strlen(buffer));
        assert(n > 0);
        (void)n;
    }

    close(wfd);
    return 0;
}
.PHONY:all
all:server client

server:server.cpp
	g++ -o $@ $^ -std=c++11 -g

client:client.cpp
	g++ -o $@ $^ -std=c++11 -g

.PHONY:clean
clean:
	rm -f server client 

需要注意的是,同时打开两个进程之后,因为在设计代码的时候,client端是没有"删除"管道这一函数的,所以尽量保证先退出client端。client端退出后,server端检测到管道的写端关闭,就会终止通信,从而调用"删除"管道的函数。如果不按这个顺序去做,很有可能管道文件依旧"躺"在磁盘上。

同样的,cliet端并没有创建管道的函数,所以会直接open一个无效的管道文件。所以,尽量保证先打开server端,再打开client端。

尽量不要两个进程都创建同一个管道

3.5system V共享内存

共享内存是进程间通信最快的通信方法。与管道不同的是,其声明周期跟随操作系统。原理如下:

        1.通过系统调用在内存开辟一块共享内存

        2.将此内存地址通过页表映射到不同进程的地址空间中

        3.进程拿到虚拟地址直接读写通信

很显然,共享内存可以让任意进程通信。我们把共享内存地址映射到进程的地址空间的行为称为挂接;将此映射关系删除称为去关联。

272a2e759b3245a8933e9031f7b0fee8.png

内存的开辟是需要进程调用系统调用的,我们不能直接这么做(试想一下进程A开辟了内存,进程B怎么看到进程A开辟的内存?)。那么实现共享内存通信的具体步骤如下:

        1.通信双方的进程首先要获取一个IPC资源的唯一标识(我们称为key),可以通过ftok这个系统调用完成:

通过指定的路径名以及自己设置的id(随便写)获取一个唯一的返回值。

        2.创建共享内存。当某个进程获取到唯一标识之后,就可以使用shmget系统调用创建共享内存了:

注意第三个参数的意义:IPC_CREAT,如果内存当中不存在共享内存,那么就创建,如果存在,shmget就正常退出;IPC_EXCL,如果内存当中不存在共享内存,那么就创建,如果存在,shmget创建共享内存失败,返回-1,这个不能单独使用。IPC_CREAT通常用于非创建端的进程;IPC_EXCL用于创建端的进程,因为创建端的进程有必要保证创建的共享内存是新的。

shmget的返回值是标定创建好的内存的标识符。因为共享内存是操作系统创建的,所以也必须符合"先描述再组织"的思想。也就是说,假设第二个参数指定了4kb大小的,那么实际上创建的内存大小一定会比4kb大,因为操作系统需要对其管理,所以有必要多申请一块空间用于存放这块内存的属性(通过结构体的方式)。

调用完成后,操作系统会将唯一标识符(key)和shmget返回的标识符设置进描述共享内存的结构体当中。

还需要注意,物理内存是分块(分页)管理的,每页分为4kb。如果我们指定的大小不是4kb的整数倍,那么操作系统就会向上取整,但是我们的可用空间是我们指定的大小(例如申请20字节的大小,操作系统会提供4kb的内存,但我们只能使用20字节)。

最后需要注意,创建的共享内存是需要有权限的(与创建文件一样)。稍后在代码中演示。

         3.通信双方的进程需要挂接。挂接通过系统调用shmat完成:

第二个参数设为空,第三个参数设为0。这个系统调用就如同C语言使用malloc一样。

        4.通信完成之后先去关联。其系统调用为shmdt:

        5. 最后,"删除"共享内存。其系统调用为shmctl:

将第二个参数设为IPC_RMID即可,第三个参数设为空。

为什么删除要打个引号呢?调用这个系统调用并不是真正删除。类似于文件,只有当文件硬链接数为0的时候才被删除。

真正删除共享内存的操作是:

ipcs -m                ==>查看当前有多少共享内存

 ipcrm -m shmid                ==>删除指定标识符的共享内存

3.6System V共享内存代码实例

简化任务,这里让两个进程通信,两个进程公用一个头文件:

//command.h
#pragma once

//先指定路径名和id,以便后续获取IPC资源唯一标识符

#define PATHNAME "."//假设在当前路径
#define ID 0x66//随便给

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
#include <cstring>
#include <cerrno>

//首先获取IPC资源唯一标识符
key_t getKey()
{
    key_t key = ftok(PATHNAME,ID);
    if(key < 0)
    {
        std::cout << "ftok fail:" << strerror(errno) << std::endl;
        exit(1);
    }
    return key;
}

//再创建共享内存
int createShm(key_t key)//需要有key指定
{
    //创建端只希望创建一个新的共享内存
    int shmid = shmget(key,4096,IPC_EXCL | IPC_CREAT | 0600);
    if(shmid < 0)
    {
        std::cout << "shmid fail:" << strerror(errno) << std::endl;
        exit(1);
    }
    return shmid;
}

//非创建端的只需要获取
int getShm(key_t key)
{
    int shmid = shmget(key,4096,IPC_CREAT);
    if(shmid < 0)
    {
        std::cout << "shmid fail:" << strerror(errno) << std::endl;
        exit(1);
    }
    return shmid;
}

//挂接
void* attachShm(int shmid)
{
    void* p = shmat(shmid,nullptr,0);
    //如果挂接失败怎么办?指针类型的p如何转成-1?
    //if((int)p == -1)//这种方法是错误的。因为Linux下的指针为8个字节
    if((long long)p == -1L)
    {
        std::cout << "shmat fail:" << strerror(errno) << std::endl;
        exit(1);
    }
    return p;
}

//去关联
void deAttachShm(const void* shmaddr)
{
    int ret = shmdt(shmaddr);
    if(ret < 0)
    {
        std::cout << "shmcdt fail:" << strerror(errno) << std::endl;
        exit(1);
    }
}

//"删除"共享内存
void delShm(int shmid)
{
    int ret = shmctl(shmid,IPC_RMID,nullptr);
    if(ret < 0)
    {
        std::cout << "shmctl fail:" << strerror(errno) << std::endl;
        exit(1);
    }
}
//server.cpp

#include "command.h"
#include <unistd.h>

//假设server端负责创建、删除内存;负责读
int main()
{
    key_t key = getKey();//获取IPC资源唯一标识符
    int shmid = createShm(key);//创建共享内存
    char* p = (char*)attachShm(shmid);//挂接

    //通信
    while(true)
    {
        std::cout << "client -> server:" << p << std::endl;
        sleep(1);
    }

    deAttachShm(p);//去关联
    delShm(shmid);//删除
    return 0;
}
//client.cpp

#include "command.h"
#include <unistd.h>
//假设client端负责写
int main()
{
    key_t key = getKey();//获取IPC资源唯一标识符
    int shmid = getShm(key);//创建共享内存
    char* p = (char*)attachShm(shmid);//挂接

    //通信
    while(true)
    {
        strcpy(p,"hello i am client!");
        sleep(3);
    }

    deAttachShm(p);//去关联
    return 0;
}

运行结果如下:

可以明显的发现共享内存并不像管道那样会发生阻塞行为。其原因在于:System V共享内存并没有提供保护机制。这就会导致client端并没有向共享内存写入任何数据,但是server端却一直在读。

4.System V信号量

这里只是简单描述一些概念,并没有实质编码。

信号量的本质就是一个公共资源的计数器,表示公共资源的多少

能被多个进程同时访问的资源称为公共资源。上述的共享内存是没有任何保护机制的,也就是说client端与server在同时对共享内存进行读写操作,这样会产生数据不一致问题:假设client端要向内存写入"hello world"这个字符串,当client写到"hello"时server便从内存把这个写到一半的字符串读走了,就会造成读取的数据不完整。此时便要向公共资源提供一种保护策略,但同时又会引入新的问题,通过不断地优化,直到这些问题能被用户接收,所以就诞生出了各种保护策略的特性和适用场景。

我们将被保护的公共资源称为临界资源。资源存在的意义便是要被使用,进程使用资源的方式是通过代码,进程一定有对应的代码访问这些资源。访问临界资源的代码叫做临界区

如何保护公共资源?有一种方法叫做互斥,互斥即只能让一个进程完整地访问临界资源,只有当此进程访问完之后才能让另一个进程进行访问。互斥通常由锁完成。

那为什么需要信号量呢?信号量是一个计数器,标定了公共资源的多少。当某个进程要访问公共资源时,它必须先申请一个信号量,此时信号量计数器--,表明公共资源少了一份,只有当进程拿到信号量之后才能访问公共资源。例如上面的例子,假设为共享内存添加信号量计数器,所以它的计数器为1(因为共享内存只有一份),然后client端要求访问这块内存,它就必须申请一个信号量,然后信号量计数器由1变为0,表明这份资源已经没有了、被其他进程拿走了。此时server端想要对这块内存进行读操作,它也会先申请一个信号量,但很可惜,信号量的计数器为0,所以server端会申请信号量失败,从而无法访问这块内存。当client端访问完毕之后,会归还信号量,信号量的计数器由0置1,此时server端才能访问这块内存。由此可见,信号量是可以完成互斥任务的

我们把进程申请信号量成功,计数器成功-1的操作称为p操作;进程访问资源结束后,释放资源让计数器+1的行为称为v操作。

既然信号量能被多个进程访问,那它必然也是一个公共资源,也必须有对应的保护机制。信号量计数器++、--的操作是原子性的,所以它并不会受其他进程的影响。原子性是什么?任何只具有两态的事件(要么不做任务,要么做任务就一口气做完)、中间没有任何过程(在外部看来)的特性的称为原子性

事实上,公共资源可以划分成一个一个的小资源,再配合信号量,在一定程度上可以达到并发。

信号量的使用需要程序员写代码确认进程具体访问哪个资源。

5.操作系统如何维护IPC资源

Linux管理的方法为"先描述再组织"。操作系统维护IPC资源都必须创建对应的结构体。并且它们的接口相似度非常高。

前面提到过内核唯一标识符key,这就是操作系统管理IPC资源的入口。而描述key的结构体是一模一样的:

由此可见,操作系统内部有一个 struct ipc_prem* 类型的指针数组,从而维护IPC资源。再加上C语言的特性:结构体当中的首成员地址与结构体地址在数值上是相等的。所以操作系统拿到对应的struct ipc_prem结构体之后(这个结构体的对象就是IPC结构体的第一个成员),就能对IPC资源进行操作。当然,即使各种IPC类型都不一样,操作系统还是有办法分辨的,拿到地址后只需要简单进行强制类型转换即可。这就是多态!

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小龙向钱进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值