操作系统之进程间通信


    进程间通信:
/************************    
    当出现白色的a.out文件且无法在虚拟机和电脑上删除该文件时:
    1.到该目录下编译指令    【ps -ef】找到该.out进程的进程号
    2.用指令    【kill -9 该进程的进程号】    终止该进程
***************************/    
    
1.基本概念:
    两个/多个进程之间数据交流/交换,称为进程间通信
    由于进程的地址空间是相互独立的,进程间通信,就需要其它机制
    
2.通信方式:
    1.信号
    2.管道    

    3.共享内存
    4.消息队列
    5.信号量
    
    6.套接字
——————————————————————————
    
一.信号:【给进程用的,进程使用信号就像人使用手机短信】

1.中断:
    表示暂时停止当前进程的执行转而去执行新的程序或处理意外情况的过程,叫做中断
    中断分为硬件中断和软件中断

2.基本概念:
    信号的本质就是软中断,它可以作为进程中通信的一种机制,更重要的是,信号总是中断一个
    【正在执行】的程序,它更多的被用于处理意外情况。
    
3.基本特点:
    1.信号是异步的,进程不知道信号什么时会到
    2.进程既可以处理信号,也可以忽略,还可以发送信号给其它进程
    3.每个信号都有一个名字,并且使用SIG开头
    
4.基本命令
    kill -l //查看系统所支持的所有信号
    常用信号:
    ctrl+c     发送信号SIGINT    2    默认处理方式为终止进程
    ctrl+\    发送信号SIGQUIT    3    ...
    【kill -9    发送信号SIGKILL    9    ...】
    kill  -9 pid 指令用于结束ID号为pid的进程        【非常常用】
    
    7     SIGBUS    总线错误
    11    SIGSEGV    段错误
    13    SIGPIPE    管道破裂
    14    SIGALRM    定时器信号
    17    SIGCHILD 子进程结束时给父进程发的信号
        
5.信号的分类
    一般来说,linux支持的信号范围是1-64,不保证连续
    而unix支持的信号范围是1-48
    其中1-31之间的信号,叫不可靠信号,不支持排队,信号可能丢失,也叫非实
    时信号 
    34-64之间的信号,叫可靠信号,支持排队,不丢失,也叫实时信号
    
    生成信号的事件来分:程序错误(如非法访问内存)、外部事件(ctrl+c)、显示
    请求(kill -9)

6.处理信号 
    1.默认处理        绝大多数的信号默认处理方式都是终止进程
    2.忽略处理        SIGSTOP和SIGKILL不能被忽略
    3.自定义处理    只要写一个信号处理函数即可    
注:    
    自定义处理即使用    signal(SIGCHLD,sigHander);
    若程序中没有该语句,则视为对SIGCHLD信号作默认处理【终止进程】
    若有该语句,则当SIGCHLD信号出现时,程序将执行sigHander函数,
    而不会作默认处理【不会终止进程】
    
信号的使用步骤:
1.写一个信号处理函数
【即封装一个函数,当信号来了时需要做什么就写入该函数中】
void sigHander(int signo)
{
    //信号来了时要做的工作
}
signo:信号的数值【1-64】

2.注册该信号函数        signal
#include <signal.h>

typedef void (*sighandler_t)(int);//函数指针

    sighandler_t signal(int signum, sighandler_t handler);
    功能:向系统注册信号处理函数(即第二个参数),只要有信号到来,系统
    就会自动调用函数指针对应的函数
例如:
    signal(SIGCHLD,sigHander);
    SIGCHLD:信号的名称/数值
    sigHander:信号的处理方式【函数指针    自定义的处理方式】
其中,信号的处理方式分为三种:
        SIG_IGN        忽略处理
        SIG_DFL        默认处理【一般效果都是终止进程】
        函数指针    自定义的处理方式
注:若是不注册信号函数,则程序碰到这个信号时就按照默认处理来
处理这个信号!!
若是注册了这个信号,就按照注册的方法处理这个信号【分三种】

3.设置闹钟,等过了指定时间后再发送信号
    unsigned int alarm(unsigned int seconds);
    功能:主要用于根据参数指定的秒数之后发送SIGALRM信号
    返回值:成功返回上一个闹钟没有来得及响的秒数,之前没有闹钟则返回0
    当参数为0时,则之前设置的闹钟会被取消
    例如:
    int res = alarm(10);            //10秒后发送时钟信号    
    printf("res = %d\n",res);//打印上一个时钟没来得及相应的秒数    【0】
    sleep(2);    //休眠2秒
    res = alarm(5);        //5秒后发送时钟信号        并返回上一个时钟没来得及相应的秒数
    printf("res = %d\n",res);//打印上一个时钟没来得及相应的秒数    【10-2=8】    
    sleep(3);
    res = alarm(0);    //取消之前设置的闹钟                                                             
    printf("res = %d\n",res);//    打印上一个时钟没来得及相应的秒数    【5-3=2】                                                     
    //信号发送前取消信号将不会发送时钟信号                                                             
                                                                 
4.等待一个信号,会导致调用进程休眠,直到收到一个信号
    int pause(void);                                                                     
    功能:用来等待一个信号,会导致调用进程休眠,直到收到一个信号
    例如:
    pause();    //死等一个信号的到来
    printf("hello world\n");    //信号到来后打印hello world
    注意:该信号必须要以自定义处理方式注册,
    否则该信号到来后将会终止进程,使等待变得毫无意义
                                                                 
5.给正调用的进程发送参数指定的信号                                                                 
    int raise(int sig);
    sig:信号名
    功能:主要用于给正调用的进程发送参数指定的信号                                                             
    例如:raise(kill);
        //杀死正在调用的进程【自杀】
                                                                 
6.给指定的进程发送指定的信号                                                                 
    int kill(pid_t pid, int sig);                                                             
    功能:主要用于给指定的进程发送指定的信号                                                             
    例如:        kill (pid,signo);                                                             
    pid    :进程号        
    signo:信号名

    raise(int sig);    与    kill(getpid(), sig);    等价

7.system    在程序中调用系统命令(和shell命令)。 
 system("pause")就是从程序里调用“pause”命令; 
 而“pause”这个系统命令的功能很简单,就是在命令行上
 输出一行类似于“Press any key to exit”的字,等待用户按一个键,
 然后返回。

二.管道【也称管道文件,本质是一个文件】
基本概念
    【管道本质】还【是】以【文件】作为通信的媒介,该文件比较特殊,称为管道文件
    管道分为两类:
        无名管道:由内核创建,实现具有亲缘关系(父子或兄弟)的进程之间的通信       pipe
        
        有名管道:由程序员创建,实现任意两个进程之间的通信                        FIFO
        
    管道的创建与说明:
        管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fds[0]和fds[1]
        其中,fds[0]固定用于读管道,fds[1]固定用于写管道,这样构成一个半双工的通道。
        
        管道关闭时,只需将这两个文件描述符关闭即可。
        
管道的使用步骤:

1.创建无名管道【本质是生成一个(管道文件)】        pipe();
    int pipe(int pipefd[2]);
    功能:用来在内核中创建一个无名管道,
    实现具有亲缘关系(父子或兄弟)的进程之间的通信 
    
    返回值:    成功:返回 0
                失败:返回 -1  ,errno被设置    
    如果创建成功,则会 把读和写的文件描述符返回给pipefd[0]和pipefd[1];            
    例如:
        int pipefd[2];
        int res = pipe(pipefd);
        pipefd:    型数组,用来存放管道的读、写文件描述符
        pipefd[0]:用于读管道
        例如:read(fds[0],buf,res);
        //从管道文件中读res个字节到buf区
        
        pipefd[1]:    用于写管道
        例如:res = write(fds[1],str,strlen(str));
        //将str中数据写入管道文件中
注:
        1.pipe创建的管道是阻塞式的
            当【读】管道时,如果管道中没有数据,则阻塞,直到有数据到来
            当【写】管道时,如果管道缓冲区满,则阻塞,直到有进程读起数据,则继续写
            
        2.    数据写到管道中,内核会把这些数据缓存,直到有进程来读
            
        3.    写端对读端的依赖性:
                只有在管道的读端存在时,向管道写数据才有意义,否则,向管道中写数据的进程将收到内核传
                来的信号SIGPIPE【管道破裂】,应用程序可以处理该信号,也可以忽略(默认是终止进程)
                //当管道的读端关闭时,往管道里写数据会造成管道破裂,
                //即会发出信号SIGPIPE,默认会终止进程        
        
2.创建有名管道【本质是生成一个(管道文件)】    mkfifo()    ;
    http://www.cnblogs.com/TsengYuen/archive/2012/05/16/2504102.html
    
    int mkfifo(const char *pathname, mode_t mode);    
    功能:创建一个有名管道    ,实现任意两个进程之间的通信
        
    返回值:    成功:返回 0
                失败:返回 -1  ,errno被设置    
    例如:    
    int res = mkfifo("/home/fifo114",0664);    
    /home    :内存中确实存在的一个目录的绝对路径    
    fifo114    :创建的管道文件的文件名    
    "/home/fifo114"    :在home目录下创建一个管道文件    
    0664        :该管道文件的权限    
    /**************************
    最开始”/home“下并不存在管道文件"fifo114"
    执行代码mkfifo("/home/fifo114",0664);后
    路径”/home/csgec“下才存在管道文件"fifo114"
    ****************************/    
无名管道和有名管道的对比:
1.作用:
        无名管道:只能实现具有情缘关系的进程间的通信
        有名管道:可以实现任意两个进程间的通信
2.创建方式:
        无名管道:需要提供一个int 型数组,用来存放管道的读、写文件描述符
            int pipefd[2];
            int res = pipe(pipefd);
        有名管道:需要提供一个内存中确实存在的一个目录的绝对路径
            int res = mkfifo("/home/fifo114",0664);    
3.通信方式【即如何读写    】:    
        无名管道:不需要打开管道文件,可以直接对文件进行读写
            pipefd[0]:用于读管道
            例如:read(fds[0],buf,res);
            //从管道文件中读res个字节到buf区            
    —        pipefd[1]:    用于写管道
            例如:res = write(fds[1],str,strlen(str));
            //将str中数据写入管道文件中    
        有名管道:必须先打开管道文件才能对文件进行读写
            int fd = open("/home/csgec/fifo114",O_WRONLY);
            //打开管道文件,权限只写    
            char *str = "hello world!";
            res = write(fd,str,strlen(str));
            //将str中数据写入有名管道所在文件    
    —        int fd = open("/home/csgec/fifo114",O_RDONLY);
            //打开有名管道所在文件,权限只读    
            char buf[1024] = {0};
            res = read(fd,buf,sizeof(buf))    ;
            //从有名管道读取数据到buf中    
    
三.共享内存        share memory
【本质】:    是属于内核的一块内存空间,这块空间由内核维护和管理,
                但是其他进程可以通过内存映射的方式对该内存进行操作
                (进程拥有的内存是4G的虚拟内存,若不与内核的内存建立映射关系,
                就不能对内核产生影响,操作也毫无意义。)
基本概念:
    共享内存是以一块内存作为通信的媒介,这块内存由内核维护和管理,允许其他进程映射。
    共享内存是进程间通信方式中效率最高的。
    共享内存最大的问题就是多个进程同时修改时,很难控制。
    
共享内存的使用步骤:
1.获取key        ftok();
【本质】:key是一个IPC,由ftok函数利用【实际存在的文件的identity(即文件id),
并将其最后8bit位同proj_id做与运算】生成

    key_t ftok(const char *pathname, int proj_id);
    key_t ftok("文件名",'整形的项目ID');    //a也是整数
    功能:ftok函数利用实际存在的文件的identity(即文件id),并将其最后8bit位
    同proj_id做与运算生成一个唯一的IPC key了。这个key可代表system v的消息队列、
    信号灯、共享内存的描述符。
    返回值:成功:返回key_t类型的key,
                失败:返回-1 ,errno被设置
    例如:key_t key = ftok(".",'a');    //获取KEY
    ".":字符串形式的文件路径,要求必须存在且可访问
    'a':整型的项目ID,要求非0,低8位被使用,一般写一个字符

2.通过key获取一块共享内存        shmget();
    int shmget(key_t key, size_t size, int shmflg);
    即: shmget(key,共享内存大小,权限);
    例如:int shmid = shmget(key,1024,0666 | IPC_CREAT);

    功能:主要用于创建/获取共享内存
    返回值:成功返回shmid,(类似打开/创建文件时获得的文件描述符)
            失败返回 -1 ,errno被设置
    key:标识共享内存的键值(类似于文件名称),一般通过ftok函数来获取键值
    size:要获取的共享内存的大小
    shmflg:标志包括IPC_CREAT与IPC_EXCL
                与open()函数的O_CREAT与O_EXCL类似
            IPC_CREAT        如果共享内存不存在,则创建,否则,直接打开
            IPC_EXCL        与IPC_CREAT结合使用,如果存在,则报错
                                    (IPC_CREAT | IPC_EXCL)
        注:当新建共享内存时,需要在第三个参数中指定权限,一般    0666 | IPC_CREAT | IPC_EXCL
        
3.相关指令
        ipcs -m    
        //显示所有共享内存段
------------ 共享内存段 --------------
键                    shmid      拥有者      权限             字节     连接数        状态      
0x00000000 1179648    gec        600        524288     2          目标       

        ipcrm -m id
        //移除共享内存段id【id即为查看到的shmid】

4.映射共享内存        *shmat();
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    即:*shmat(共享内存ID,共享内存映射到的目标地址,标志);
    功能:用于映射共享内存到用户空间
    返回值:    成功:返回共享内存的映射地址
                失败:返回 -1,errno被设置

    例如:void *addr = shmat(shmid,NULL,0);
    //    将共享内存shmid的ID映射到NULL,该内存可读可写
    //生成新指针,指向该地址
    shmid:获取到的共享内存的ID,shmget函数的返回值
    NULL:将共享内存映射到指定地址,一般给NULL即可
    0:标志,默认给0,表示可读可写

5.操作共享内存
    写:    char *str = "hello world";
        1.    memcpy(addr,str,strlen(str));
        2.    strcpy(addr,str);        
        3.    sprintf(addr,"%s",str);
    读:printf("buf =%s\n",(char *)addr);

6.解除共享内存        shmdt();
【目的】解除共享内存是为了防止在以后使用进程的时候对内核进行误操作
    int shmdt(const void *shmaddr);
    即:shmdt(指向需要解除的共享内存的指针);
    功能:用于解除映射
    例如:shmdt(addr);
    //解除addr指向的内存
    addr:shmat的返回值【映射到的地址】
        
7.删除共享内存对象        shmctl();
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    即:shmctl(共享内存的ID,cmd操作命令,结构体指针);
    功能:用于对指定的共享内存执行指定的操作
    返回值:    成功:返回0
                失败:返回-1,errno被设置
    例如:shmctl(shmid,IPC_RMID,NULL);            
    shmid:共享内存的ID,shmget函数的返回值                                    
    IPC_RMID:cmd操作命令
        IPC_RMID    删除共享内存对象,此时第三个参数给NULL即可
    NULL:结构体指针

四.消息队列
/*****************************
发送消息和接收消息时,key的值一样,所以要用同一条代码去生成key
如:    key_t key = ftok("/home",'b');
其中路径必须存在!!!
*****************************/
【本质】:是【系统内核中的】一个用来存放消息的【队列】
【工作原理】:    【将数据打包成消息,将消息放到队列中】,
                        使【两个进程通过访问这个队列】从而实现进程通信。
基本概念
    消息队列就是系统内核中保存的一个用来【将数据打包成消息,
    将消息放到队列中】,使【两个进程通过访问这个队列】从而实现通信。
    
消息队列的使用步骤
1.创建/获取消息队列            msgget();
    int msgget(key_t key, int msgflg);
    即:msgget(key,消息队列的创建标志);
    功能:主要用于创建/获取消息队列 
    返回值:成功:返回创建的消息列队的ID
                失败:返回-1,errno被设置
    例如:int msgid = msgget(key,0666|IPC_CREAT);
        //得到一个消息队列的ID,用msgid保存
    key:标识共享内存的键值,即ftok函数的返回值                                
    0666|IPC_CREAT:消息队列 的创建标志
        IPC_CREAT        如果共享内存不存在,则创建,否则,直接打开
        IPC_EXCL            与IPC_CREAT结合使用(IPC_CREAT | IPC_EXCL),如果存在,则报错
        0 获取已存在的消息队列
    注:当新创建消息队列时,需要指定权限,如0666|IPC_CREAT    
    
2.生成一个消息【即定义一个消息类型的结构体,并给其赋值】
    struct msgbuf msg;    //定义一个消息型结构体变量用来存储要发送的消息
    msg.msgtype = 1;        //该消息类型赋值为1
//    msg.buf = "hello";        //除了初始化之外,不能直接整体给数组赋值
    strcpy(msg.buf,"hello world");    //赋值消息的内容


    
3.发送消息        msgsnd();        
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    即:msgsnd(得到的一个消息队列的ID,消息的地址,消息的大小,发送标志);
    功能:用于向指定的消息队列发送指定的消息
    例如:int res = msgsnd(msgid,&msg,sizeof(msg.buf),0);
    //将地址msg中的内容的sizeof(msg.buf)个字节以阻塞的方式发送给ID为msgid的消息队列
    msqid:msgget函数的返回值
    &msg:消息的地址(消息从哪里来)
        消息的一般形式:
        struct msgbuf
        {
            long msgtype;//消息类型,需要大于0
            char mtext[1];//消息的内容,可以使用其它的数据类型
        };
    sizeof(msg.buf):消息的大小(指消息的大小,不包括类型)
    0:发送标志,一般给0(阻塞直到发送成功)    

4.接收消息            msgrcv();
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    即:msgrcv(得到的一个消息队列的ID,消息的地址,消息的大小,消息的类型,接受的标志);
    功能:用于接收消息
    例如:struct msgbuf msg;    
    //定义一个消息型结构体变量用来存储接收到的消息
    msgrcv(msgid,&msg,sizeof(msg.buf),0,0);
    //以阻塞的方式接收消息队列msgid中的sizeof(msg.buf)个类型为0的消息,
    //并存入消息型结构体变量msg中
    msgid:msgget函数的返回值
    &msg:消息的地址(消息保存到哪里去)
    sizeof(msg.buf):消息的大小
    0:消息的类型
        0         表示接受队列中的第一个消息
        >0         表示接受队列中第一个类型为msgtyp的消息
        <0         表示接受队列中第一个小于等于msgtyp绝对值的消息,
        其中最小的类型优先读        
    0:接受的标志, 一般给0    //0代表阻塞

5.删除队列            msgctl();            
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    即:msgctl(得到的一个消息队列的ID,cmd操作命令,结构体指针);
    功能:用于对指定的消息队列 执行指定的操作
    返回值:    成功:返回0
                失败:返回-1,errno被设置
    例如:msgctl(msgid,IPC_RMID,NULL);
    msgid: msqid,msgget函数的返回值
    IPC_RMID: cmd操作命令
        IPC_RMID    删除消息队列,此时第三个参数给NULL即可
    NULL:结构体指针

6.相关指令:    
        ipcs -q
--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息  
          
        ipcrm -q id 
        //移除消息队列id【id即为查看到的msqid】

五.信号量    
/*********************
信号量就相当于共享内存中的一个盒子,
把信号量初始化成多少就是最开始的时候往盒子里放了多少个信号!
P操作就是从盒子里拿信号,只有盒子里有信号时才能拿,
若盒子里没有信号,你就只能在盒子旁边等到什么时候盒子里有信号了,拿到信号才能走!
V操作就是往盒子里放信号,不管盒子里有没有信号,你都可以往里面放信号!
*********************/
【本质】:信号量是在计算机中用来模拟信号灯的一个【整数】
当这个整数变为非0才能进行某种操作许可;
在进行操作的同时,将该整数减1,以此改变信号灯,
将减1操作称为P操作,也称获取信号;
当该操作进行完之后,将该整数加1,以此来恢复信号灯,
将加1操作称为V操作,也称释放信号;    
    
【原理】:可以模拟为一下代码:
int i=N;        //初始化信号量
if(i != 0)
{
    i--;    //可以进行P操作
    //i++    //也可以进行V操作
    进行某种操作.....
    i++;    //V操作
}
else    if(i == 0)
{
    i++;    //只能进行V操作
}

【1】.进程中信号量的使用步骤
1.获取key,使用函数ftok()
key_t key=ftok("/home",'a');
"/home":字符串形式的文件路径,要求必须存在且可访问
'a'    :整型的项目ID,要求非0,低8位被使用,一般写一个字符

2.创建/获取信号量,使用函数semget()函数
int semid = semget(key,1,0777 | IPC_CREAT);
key:    ftok的返回值
1:        信号量的个数
0777 | IPC_CREAT:    标志,可用    IPC_CREAT   IPC_EXCL    
注:创建新的信号量时,需要指定权限,如0664 | IPC_CREAT

3.初始化信号量,使用函数semctl()函数
int res = semctl(semid,0,SETVAL,1);
if(res < 0)
{
    perror("semctl");
    exit(-1);
}
semid:semid,semget的返回值
0:信号量的下标
SETVAL:操作命令
    IPC_RMID        删除信号量,不需要第四个参数
    IPC_SETVAL        使用第4个参数的值初始化信号量
1:可变参数,是否需要由第三参数决定 

4.定义结构体指针
    struct sembuf
    {
        unsigned short sem_num;  /* 信号量所在下标 */
        short sem_op;   /* 具体操作,正数增加,负数减少*/
        short sem_flg;  /* 标志 IPC_NOWAIT and SEM_UNDO. */
    };

5.初始化结构体指针
P操作:
sb.sem_num = 0;        
sb.sem_op = -1;        //P操作将其初始化为-1
sb.sem_flg = SEM_UNDO;
V操作:
sb.sem_num = 0;
sb.sem_op = 1;            //V操作将其初始化为1
sb.sem_flg = SEM_UNDO;

6.操作信号量,使用函数semop()函数
int res = semop(semid,&sb,1);
if(res < 0)
{
    perror("semop");
    exit(-1);
}
semid:获取的信号量
&sb:结构体指针
    struct sembuf
    {
        unsigned short sem_num;  /* 信号量所在下标 */
        short sem_op;   /* 具体操作,正数增加,负数减少*/
        short sem_flg;  /* 标志 IPC_NOWAIT and SEM_UNDO. */
    };
1:结构体指针指向的结构体数组的元素个数

ps:访问共享资源

7.删除信号量,使用函数semctl()函数
int semctl(semid,0,IPC_RMID);
semid:获取的信号量
0:信号量的下标
IPC_RMID:删除信号量,不需要第四个参数    
    
【2】.线程中信号量的使用步骤    
1.定义一个信号量
sem_t    sem;

2.初始化信号量    sem_init()函数
sem_init(&sem,0,1);
&sem:    定义的信号量的地址
第二个参数:0:表示同一进程中的线程间共享,非0:进程间共享
1:    信号量的初始值
注:sem处信号量为同一进程中线程共享的信号量,
信号的初始值为1,即P、V操作从1开始

3.获取信号量(即P操作)
sem_wait(&sem);
&sem:定义的信号量的地址

ps:访问共享资源

4.释放信号量(即V操作)
sem_post(&sem);
&sem:定义的信号量的地址

5.销毁信号量
sem_destroy(&sem);
&sem:定义的信号量的地址    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

                        
                                                                 
                                                                 
                                                                 
                                                                 
                                                                 

猜你喜欢

转载自blog.csdn.net/qq_26128879/article/details/82890207