进程间通信

进程是Linux操作系统环境的基础,它控制着系统上几乎所有的活动。进程基础概念

  • 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用
  • 僵尸进程以及如何避免僵尸进程
  • 进程间通信 最简单的方式: 管道
  • 3种system V进程间通信方式: 信号量、消息队列和共享内存。
  • 在进程间传递文件描述符的通用方法:通过unix本地域socket传递特殊的辅助数据。

一 管道

       管道是父进程和子进程通信的常用手段。
int pipe(int pipefd[2]);

管道能在父子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭读端,另外一个关闭写端。
如果要实现父子进程之间的双向数据传输 我们可以用socketpair函数。

int socketpair(int d, int type, int protocol, int sv[2])

该函数返回的文件描述符是全双工通信,所以只用打开一个就可以实现父子进程间的双向数据传输,但是注意一点,如果父进程用sv[0]写入,则子进程一定要用sv[1]读出。
在之前博客统一事件源中就用到了该函数的例子,感兴趣的可以去看看。

二 信号量

当多个进程同时访问系统上的某个资源时,比如同时写一个数据库的某条记录,或者同时修改文件,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,程序对共享资源的访问的代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。我们称这段代码为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。
信号量的提出是并发编程领域迈出的重要一步。
假设有信号量SV。

  • P(sv),如果sv的值大于0,就将它减1;如果sv的值为0, 则挂起进程的执行。
  • V(sv),如果有其它进程因为等待sv而挂起,则唤醒之;如果没有,则将sv加1。
    在这里插入图片描述
    如上图所示,当关键代码段可用时,二进制信号量sv的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行了P(sv)操作将sv减1,则进程B若再次执行P(sv)就会被挂起。直到进程A离开代码段,并执行V(sv)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV而被挂起,则它将被唤醒,并进入关键代码段。

会用到如下三个函数.

#include<sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
/*
key参数是一个键值,用来标识一个全局唯一的信号量集,就像文件名全局唯一的标识一个文件一样。
要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。
num_sems参数指定要创建/获取的信号量集中信号量的数目。如果是创建信号量,则该值必须被指定;
如果是获取,则可以把它设置为0.
sem_flags参数指定一组标志。它低端的9个比特是该信号量的权限,其格式和含义都与系统调用open的mode参数相同。
semget成功时返回一个正整数值,它是信号量集的标识符;semget失败返回-1,并设置errno。
*/
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
/*
sem_id参数就是sem_get函数返回的值。

struct sembuf
{
	unsigned short int sem_num;
	short int sem_op;
	short int sem_flg;
};
*/

  • 其中,sem_num成员是信号集中信号量的编号,0表示信号量集中的第一个信号量。

  • sem_op成员指定操作类型,其可选值为正整数、0和负整数。每种类型的操作行为又受到sem_flg成员的影响。

  • sem_flg的可选值是IPC_NOWAIT和SEM_UNDO。IPC_NOWAIT的含义是,无论信号量操作是否成功,semop调用都将立即返回,类似于非阻塞I/O操作。
    SEM_UNDO的含义是,当进程退出时取消正在进行的semop操作。
    sem_op和sem_flg将按照如下方式来影响semop的行为
    1、如果sem_op大于0,则semop将被操作信号量的值semval增加sem_op。
    2、如果sem_op等于0,则表示这是一个等待0操作。该操作要求调用进程对操作信号量集拥有读权限。如果此时信号量的值为0,则调用立即返回。
    3、sem_op小于0,则表示对信号量值进行减操作,既期望获得信号量。该操作要求调用进程对被操作信号量集拥有写权限。如果信号量的值semval大于或等于sem_op的绝对值,则semop操作成功,调用进程立即获得信号量,并且系统将该信号量的semval值减去sem_op的绝对值。

  • 第三个参数num_sem_ops指定要执行的操作个数,既sem_ops数组中的元素个数。semop对sem_ops中的每个成员按照数组顺序依次执行操作,该过程为原子操作

看上面的标准解释可能云里雾里。简单的描述总结一下就是: 当我们想要获得这个信号量的时候我们可以将sem_op的值设为小于0,
这个时候内核就会检查信号量的semval值,如果绝对值大于等于sem_op我们就允许这段代码继续运行,这个时候有其它的进程想要接着获得该信号量则会被阻塞住,
当我们运行完这段代码后,就将sem_op的值设置为大于0,将其semval增加为大于0,这个时候其它进程会竞争然后获得该信号量。这就是用信号量实现进程同步的原理。
需要注意的是所有进程sem_op的正负值一定要为相反数。

看完解释如果还不是很懂,大家可以看看下面的例子,然后再结合概念进行理解。

int semctl(int sem_id, int sem_num, int command, ...);
/*
sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的信号量集。
sem_num参数指定被操作的信号量在信号量集中的编号。
command参数指定要执行的命令。
有的命令需要第四个参数。第四个参数的类型由用户自己定义,但sys/sem.h头文件给出了它的推荐格式

union semun
{
	int val; 用于SETVAL命令
	struct semid_ds* buf; 用于IPC_STAT和IPC_SET命令
	unsigned short* array; 用于GETALL和SETALL命令
	struct seminfo* _buf; 用于IPC_INFO命令
};
SETVAL: 将信号量的semval值设置为semun.val,同时内核数据中的semid_ds.sem_ctime被更新
IPC_STAT:将信号量集相关联的内核数据结构复制到semun.buf中
IPC_SET:将semmun.buf中的部分成员复制到信号量集关联的内核数据结构中,同时内核数据中的semid_ds.ctime被更新
*/

简单的例子
semget的调用者可以给其它key参数传递一个特殊的键值IPC_PRIVATE(其值为0), 这样无论信号量是否已经存在,semget都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字一样是进程私有的。其它进程,尤其是子进程,也有方法来访问这个信号量。
编译时记得 -lrt

#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/sem.h>

union semun
{
    int val;
    struct semid_ds* buf;
    unsigned short int* array;
    struct  seminfo* _buf;    
};

//op为-1时执行获取信号量操作,op为1时执行释放信号量操作
void pv(int sem_id, int op)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = op;    
    sem_b.sem_flg = SEM_UNDO;
    semop(sem_id, &sem_b, 1);
}

int main(int argc, char* argv[])
{
    int sem_id = semget(IPC_PRIVATE, 1, 0666);

    union semun sem_un;
    sem_un.val = 1; //将信号值设置为1
    semctl(sem_id, 0, SETVAL, sem_un);

    pid_t pid = fork();
    if(pid < 0)
        return -1;
    else if(pid == 0)    //子进程运行
    {
        std::cout << "chlid try to get binary sem \n";
        /*在父、子进程间共享IPC_PRIVATE信号量的关键就在于二者都可以操作该信号量的标识符sem_id*/
        //因为信号值为1,所以想要获得信号量就让sem_op小于0,且信号值减sem_op的绝对值要小于等于0。
        pv(sem_id, -1);
        std::cout << "child get the sem and would release it after 5 seconds \n";
        sleep(5);
        //当处理完成后,将信号量恢复。
        pv(sem_id, 1);
        exit(0);
    }

    else
    {
        std::cout << "parent try to get binary sem\n";
        pv(sem_id, -1);
        sleep(5);
        std::cout << "parent get the sem and would release it after 5 seconds\n";
        pv(sem_id, 1);
        std::cout << "parent will sleep 3 seconds\n";
        sleep(3);
        std::cout << "parent weak and will exit\n";
    }

    waitpid(pid, NULL, 0);  //回收子进程结束的资源 避免僵尸进程的产生
    semctl(sem_id, 0, IPC_RMID, sem_un); //删除信号量
    return 0;

}

三 共享内存

共享内存的POSIX方法

mmap函数:

#include<sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

int munmmap(void* start, size_t length);

具体可以参考这里
利用它的MAP_ANONYMOUS标志我们可以实现父、子进程之间的匿名共享内存。通过打开同一个文件,mmap也可以实现无关进程之间的内存共享。Linux提供了另外一种利用mmap在无关进程之间共享内存的方式。这种方式无须任何文件的支持,但它需要先使用如下函数来创建或打开一个POSIX共享内存对象。

#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
int shm_open(const char* name, int oflag, mode_t mode);

shm_open的使用方法和open系统调用完全相同。

  • name参数指定要创建/打开的共享内存对象。从可移植性的角度考虑,该参数应该使用"/somename"的格式:
  • oflag参数指定创建方式。它可以是下列标志中的一个或者多个按位或
    O_RDONLY。以只读方式打开共享内存
    O_RDWR。以可读、可写方式打开共享内存
    O_CREAT。如果共享内存不存在,则创建之。此时mode参数的最低9位将指定该共享内存对象的访问权限。共享内存被创建的时候,其初始长度为0。
    O_EXCL。和O_CREAT一起使用,如果有name指定的共享内存已经存在,则shm_open调用返回错误,否则就创建一个新的共享内存对象。
    O_TRUNC。如果共享内存对象已经存在,则把它截断,使其长度为0。
  • shm_open调用返回一个文件描述符。该文件描述符可用于后续的mmap调用,从而将共享内存关联到调用进程。shm_open失败返回-1,并设置errno。

打开一个共享内存,最后也和打开文件一样关闭它。

int shm_unlink(const char* name);

该函数将name参数指定的共享内存对象标记为等待删除。当使用该共享内存对象的进程都使用munmap函数将其从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。

下面代码是一个简单例子,使用了共享内存信号量和管道。

#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<sys/sem.h>
#include<sys/wait.h>
#include<memory>
#include<sys/types.h>
static const char* shm_name = "/my_shm"; //共享内存文件名
void pv(int sem_id, int op); //用信号量控制进程间同步

class share_mem
{
public: 
    share_mem();
    ~share_mem()
    {
        shm_unlink(shm_name);
    }
    bool write_mem(char* str); //向共享内存中写入数据
    bool read_mem(int len);           //从共享内存中读取数据


public:   
    int pipefd[2];   //通知主/子进程有数据可读/可写
private:  
    char write_buf[1024];
    char read_buf[1024];
    int shmfd;      //共享内存描述符
    std::shared_ptr<char*> sha_me; //指向共享内存的首地址
    
    //信号量
    int sem_id;
    union semun
    {
        int val;
        struct semid_ds* buf;
        unsigned short int* array;
        struct seminfo* _buf;
    }sem_un;     
};

share_mem::share_mem()
{
    pipe(pipefd); //创建管道
    bzero(write_buf, sizeof(write_buf));
    bzero(read_buf, sizeof(read_buf));
    //创建共享内存
    shmfd = shm_open(shm_name, O_CREAT | O_RDWR | O_EXCL, 0666);
    //改变共享内存大小
    ftruncate(shmfd, 1024);

    char* temp = (char*)mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, shmfd, 0);
    sha_me = std::make_shared<char*>(temp);
    //创建信号量
    sem_id = semget(IPC_PRIVATE, 1, 0666);
    sem_un.val = 1; //将信号量设置为1

    semctl(sem_id, 0, SETVAL, sem_un); //将信号量的semval值设置为1
}


//当op为-1时执行获取信号量操作,-1为释放信号量操作
void pv(int sem_id, int op)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0; 
    sem_b.sem_op = op;
    sem_b.sem_flg = SEM_UNDO;
    semop(sem_id, &sem_b, 1);
}

//向共享内存写入数据
bool share_mem::write_mem(char* str)
{
    //像共享内存中写入数据,保证进程同步,op为-1 想要获取信号量
    pv(sem_id, -1);
    if(strlen(str) > 1023)
    {
        std::cout << "data is too long, the share_mem can't recv this data\n";
        return false;
    }
    else
    {
        for(int i = 0; i <= strlen(str); ++i)
        {
            (*sha_me)[i] = str[i];
        }
        std::cout << *sha_me << std::endl;
    }
    //将信号量加1,解除对信号量控制
    pv(sem_id, 1);
}

//读取数据
bool share_mem::read_mem(int len)
{
    pv(sem_id, -1);
    for(int i = 0; i <= len; ++i)
        read_buf[i] =(*sha_me)[i]; 
    std::cout << "I read buf:  " <<  read_buf << std::endl;
    pv(sem_id, 1);
}

int main()
{
    share_mem sha;
    pid_t pid = fork();
    if(pid < 0)
    {
        std::cout << "pid is false\n";
    }
    else if(pid == 0)
    {
        close(sha.pipefd[0]);
        char* str = "hello word";
        sha.write_mem(str);
        int flag = strlen(str);
        //通知主进程,共享内存中的数据写入,你可以读取
        write(sha.pipefd[1], (char*)&flag, 4);
    }
    else
    {
        close(sha.pipefd[1]);
        int flag;
        //如果共享内存中没有数据,就将阻塞住
        read(sha.pipefd[0], (char*)&flag, 4);
        sha.read_mem(flag);
    }
    int status;
    wait(&status);
    return 0;

}

结果演示
在这里插入图片描述

这里有我用共享内存实现的简单地聊天室,感兴趣的同学可以点开看一下

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值