进程间通信-IPC

1.进程间通信的方式IPC

1.早期的进程间通信:

无名管道(pipe)、有名管道(fifo)、信号(signal)

2.system V IPC:

共享内存(share memory)消息队列(message queue)、信号等集(semaphore set)

3.BSD:

套接字(socket)

2.无名管道

2.1 原理

通信原理:一个进程的输出可以当做另一个进程的输入

2.2 特点

  1. 只能用于具有亲缘关系的进程之间的通信
  2. 半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。
  3. 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
  4. 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

2.3 函数接口

int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端  fd[1]:写端
返回值:成功 0
       失败 -1

3.有名管道

3.1 特点

  1. 有名管道可以使互不相关的两个进程互相通信。
  2. 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中(也就是内核空间中)
  3. 进程通过文件IO来操作有名管道
  4. 不支持如lseek() 操作
  5. 有名管道遵循先进先出规则

3.2 函数接口

int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
       mode:权限
返回值:成功:0
       失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)

4.信号

kill -l: 显示系统中的信号

kill -num PID: 给某个进程发送信号

4.1 概念

  1. 信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
  2. 信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
  3. 如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

信号的生命周期:

4.2 信号的响应方式

1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。

2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。

3)执行缺省(默认)操作:Linux对每种信号都规定了默认操作

4.3 信号种类

SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程

SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件

SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。

SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。

SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination

SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。

SIGCONT(18):继续执行信号,用于恢复先前停止的进程。

SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。

SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。

4.4 函数接口

4.4.1 信号发送和挂起

#include <signal.h>
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
   sig:要发送的信号
返回值:成功 0     
       失败 -1

int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0   
       失败 -1
相当于:kill(getpid(),sig);

int pause(void);
功能:用于将调用进程挂起,直到收到被捕获处理的信号为止。
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    // kill(getpid(), SIGKILL);   //给指定进程发送信号,此例子是给当前进程发
    // raise(SIGKILL);                  //给当前进程发送信号

    // while (1) ;
    pause();  //将进程挂起,作用类似于死循环,但是不占用CPU

    return 0;
}

4.4.2 定时器

unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器。当定时器指定的时间到了时,它就向进程发送SIGALARM信号。
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。
常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。

系统默认对SIGALRM(闹钟到点后内核发送的信号)信号的响应: 如果不对SIGALRM信号进行捕捉或采取措施,默认情况下,闹钟响铃时刻会退出进程。

#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    printf("%d\n", alarm(10));  //设定10秒闹钟
    sleep(2);                                       //过了2秒
    printf("%d\n", alarm(3));       //此时剩余8秒闹钟时间,设了一个3秒新闹钟,新闹钟为准
    pause();            //3秒后收到SIGALRM信号结束进程,因为系统弄个对SIGALRM默认处理方式是结束进程

    return 0;
}

4.4.3信号处理函数 signal()

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
      handler:信号处理方式
          SIG_IGN:忽略信号  (忽略 ignore)
          SIG_DFL:执行默认操作 (默认 default
          handler:捕捉信号 (handler为函数名,可以自定义)
     void handler(int sig){} //函数名可以自定义, 参数为要处理的信号
返回值:成功:设置之前的信号处理方式
      失败:-1

补充:typedef给数据类型重命名

#include <stdio.h>

//给普通数据类型int重命名
typedef int size4;        

//给指针类型int* 重命名
typedef int *int_p;       

//给数组类型int [10]重命名
typedef int intArr10[10]; 

//给函数指针void (*)()重命名
typedef void (*fun_p)(); 

void fun()
{
    printf("fun\n");
}

int main(int argc, char const *argv[])
{
    size4 a = 10;             //相当于int a=10;
    int_p p = &a;             //相当于int* p=&a;
    intArr10 arr = {1, 2, 3}; //相当于int arr[10]={1,2,3};
    fun_p fp = fun;           //相当于 void (*fp)()=fun;
    printf("%d\n", *p);
    printf("%d\n", arr[0]);
    fp();

    return 0;
}

例子:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig)
{
    if (sig == SIGINT)
        printf("ctrl + c : %d\n", sig);
    if (sig == SIGTSTP)
        printf("ctrl +z:%d\n", sig);
}

int main(int argc, char const *argv[])
{
    // signal(SIGINT, SIG_IGN); //忽略信号
    // signal(SIGINT, SIG_DFL); //缺省方式
    signal(SIGINT, handler); //捕捉信号,比较常用的方式
    signal(SIGTSTP, handler);
    // while (1);
    pause();  //收到被捕获处理的信号的时候会结束挂起

    return 0;
}

5.共享内存

5.1 特点

1.共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。

2.为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。

3.由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等

5.2 步骤

  1. 创建key值
  2. 创建或打开共享内存
  3. 映射共享内存到用户空间
  4. 撤销映射
  5. 删除共享内存

5.3 函数接口

key_t ftok(const char *pathname, int proj_id);
功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存
参数:
    Pathname:已经存在的可访问文件的名字
    Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
      失败:-1

int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
    key  键值
    size   共享内存的大小
    shmflg   IPC_CREAT|IPC_EXCL|0777
返回值:成功  shmid
      出错    -1
注意对错误的处理方式:
如果错误是file exist光打开共享内存不用设IPC_CREAT|IPC_EXCL了,加判断,如:if(errno == EEXIST)

void  *shmat(int  shmid,const  void  *shmaddr,int  shmflg); //attaches
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
    shmid   共享内存的id号
    shmaddr   一般为NULL,表示由系统自动完成映射
              如果不为NULL,那么有用户指定
    shmflg:SHM_RDONLY就是对该共享内存只进行读操作
                0     可读可写
返回值:成功:完成映射后的地址,
       出错:-1(地址)
用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)

int shmdt(const void *shmaddr); //detaches
功能:取消映射
参数:要取消的地址
返回值:成功0  
      失败的-1

int  shmctl(int  shmid,int  cmd,struct shmid_ds *buf); //control
功能:(删除共享内存),对共享内存进行各种操作
参数:
    shmid   共享内存的id号
    cmd     IPC_STAT 获得shmid属性信息,存放在第三参数
            IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
            IPC_RMID:删除共享内存,此时第三个参数为NULL即可
    buf    shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null
返回:成功0 
     失败-1
用法:shmctl(shmid,IPC_RMID,NULL);

操作例子:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    key_t key;
    int shmid;
    //1.创建key值
    key = ftok("a.c", 'c');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key: %#x\n", key);
    //2.打开或创建共享内存
    shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0777); //没存在则创建共享内存,存在则返回-1
    if (shmid < 0)
    {
        //避免如果存在共享内存会报错:shmget err: File exists
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0777); //如果共享内存已存在,则直接打开共享内存然后返回该共享内存id号
        else
        {
            perror("shmget err");
            return -1;
        }
    }
    printf("shmid: %d\n", shmid);
    //3.映射共享内存
    char *p = (char *)shmat(shmid, NULL, 0);
    if (p == (char *)-1)
    {
        perror("shmat err");
        return -1;
    }

    //4. 操作共享内存
    scanf("%s", p);
    printf("%s\n", p);

    //5. 取消映射
    shmdt(p);

    //6. 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

5.4. 命令:

ipcs -m: 查看系统中的共享内存

ipcrm -m shmid:删除共享内存

注意: 可能不能直接删除掉还存在进程使用的共享内存。这时候可以用ps -ef对进程进行查看,kill掉多余的进程后,再使用ipcs查看。

6.信号灯

6.1 特点

信号灯(semaphore),也叫信号量,信号灯集是一个信号灯的集合。它是不同进程间或一个给定进程内部不同线程间同步的机制;

而Posix信号灯指的是单个计数信号灯:无名信号灯、有名信号灯。(咱们学的是无名信号灯)

System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。

通过信号灯集实现共享内存的同步操作

6.2 步骤

  1. 创建key: ftok
  2. 创建打开信号灯集: semget
  3. 初始化信号灯: semctl
  4. PV操作: semop
  5. 删除信号灯集: semctl

6.3 命令

ipcs -s: 查看信号灯集

ipcrm -s: 删除信号灯集

注意:有时候可能创建失败或者semid0所以可以命令查看删除了重新创建就可以

6.4 函数接口

int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
    nsems:信号灯集中包含的信号灯数目
    semflg:信号灯集的访问权限,通常为IPC_CREAT|IPC_EXCL|0666
返回值:成功:信号灯集ID
       失败:-1
       
int semctl ( int semid, int semnum,  int cmd…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
    semnum: 要操作的集合中的信号灯编号,信号灯编号从0开始
     cmd: 
        GETVAL:获取信号灯的值,返回值是获得值
        SETVAL:设置信号灯的值,需要用到第四个参数:共用体
        IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
      失败 -1
用法:
1. 初始化信号灯集:
需要自定义共用体
union semun{
    int val;
} mysemun;
mysemun.val = 10;
semctl(semid, 0, SETVAL, mysemun);

2. 获取信号灯值:函数semctl(semid, 0, GETVAL)的返回值
3. 删除信号灯集:semctl(semid, 0, IPC_RMID);


int semop ( int semid, struct sembuf  *opsptr,  size_t  nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
     opsptr:操作方式
     nops:  要操作的信号灯的个数 1
返回值:成功 :0
      失败:-1
struct sembuf {
   short  sem_num; // 要操作的信号灯的编号
   short  sem_op;  //    0 :  等待,直到信号灯的值变成0
                   //   1  :  释放资源,V操作
                   //   -1 :  申请资源,P操作                    
    short  sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};

用法:
申请资源 P操作:
    mysembuf.sem_num = 0;
    mysembuf.sem_op = -1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);
释放资源 V操作:
    mysembuf.sem_num = 0;
    mysembuf.sem_op = 1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);

7.消息队列

7.1 特点

消息队列是IPC对象(活动在内核级别的一种进程间通信的工具)的一种

一个消息队列由一个标识符 (即队列ID)来标识

消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等

消息队列可以按照类型(自己设一个值作为类型)来发送/接收消息

7.2 步骤

  1. 创建key: ftok()
  2. 创建打开消息队列: msgget()
  3. 添加消息: 按照类型消息添加到已经打开的消息队列末尾msgsnd()
  4. 读取消息:可以按照类型消息列表中取走 msgrcv()
  5. 删除消息队列msgctl()

7.3 操作命令

ipcs -q: 查看消息队列

ipcrm -q msgid: 删除消息队列

7.4 函数接口

int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数:  key值
       flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
       失败:-1
int msgsnd(int msqid, const void *msgp, size_t size, int flag); 
功能:添加消息
参数:msqid:消息队列的ID
      msgp:指向消息的指针。常用消息结构msgbuf如下:
          struct msgbuf{
            long mtype;          //消息类型
            char mtext[N]}//消息正文
   size:发送的消息正文的字节数
   flag:IPC_NOWAIT消息没有发送完成函数也会立即返回    
         0:直到发送完成函数才返回
返回值:成功:0
      失败:-1
使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。

int msgrcv(int msgid,  void* msgp,  size_t  size,  long msgtype,  int  flag);
功能:读取消息
参数:msgid:消息队列的ID
     msgp:存放读取消息的空间
     size:接受的消息正文的字节数(sizeof(msgp)-sizeof(long))
    msgtype:
            0:接收消息队列中第一个消息。
            大于0:接收消息队列中第一个类型为msgtyp的消息.
            小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
     flag:
           0:若无消息函数会一直阻塞
           IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
      失败:-1

int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,删除消息队列
参数:msqid:消息队列的队列ID
     cmd:
        IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
        IPC_SET:设置消息队列的属性。这个值取自buf参数。
        IPC_RMID:从系统中删除消息队列。
     buf:消息队列缓冲区
返回值:成功:0
      失败:-1
用法:msgctl(msgid, IPC_RMID, NULL

注意:有时候可能会创建失败,或者msgid为0,所以用命令看看,删了重新创建就可以了。

例子

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

struct msgbuf
{
    long type; //必须有,代表消息的类型,值>0!!!
    int num;   //消息的正文,随便定义
    char ch;
};

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;
    key = ftok("msg.c", 'b');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key: %#x\n", key);

    //创建或打开消息队列
    msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0777);
    if (msgid <= 0)
    {
        if (errno == EEXIST)           //如果消息队列已存在就直接打开消息队列
            msgid = msgget(key, 0777); //直接打开消息队列
        else
        {
            perror("msgget err");
            return -1;
        }
    }
    printf("msgid: %d\n", msgid);

    //添加消息
    struct msgbuf msg;
    size_t size = sizeof(msg) - sizeof(long);
    msg.type = 1;
    msg.num = 10;
    msg.ch = 'a';

    msgsnd(msgid, &msg, size, 0); //0:阻塞,发完消息返回

    msg.type = 2;
    msg.num = 20;
    msg.ch = 'b';
    msgsnd(msgid, &msg, size, 0);

    //读取消息
    struct msgbuf m;
    msgrcv(msgid, &m, size, 2, 0); //0:阻塞,读取到消息返回

    printf("%d %c\n", m.num, m.ch);

    //删除消息队列
    msgctl(msgid, IPC_RMID, NULL);

    return 0;
}

进程间通信

8.套接字

8.1函数接口

int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
   domain:协议族
     AF_UNIX, AF_LOCAL  本地通信
     AF_INET            ipv4
     AF_INET6            ipv6
  type:套接字类型
     SOCK_STREAM:流式套接字
     SOCK_DGRAM:数据报套接字
      SOCK_RAW:原始套接字
  protocol:协议 - 填0 自动匹配底层 ,根据type
  系统默认自动帮助匹配对应协议
     传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
     网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
 返回值:
    成功 文件描述符
    失败 -1,更新errnoy

用于后续的TCP\IP和UDP协议中,后续在详细讲,这里暂时不讲,敬请关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值