为什么需要进程间通信(IPC)?
1、数据传输
一个进程需要将它的数据发送给另一个进程。(进程要合作)
2、资源共享
多个进程之间共享同样的资源。(要协作)
3、通知事件
一个进程需要向另外一个或一组进程发送消息,通知它们发生了某种事件(进程同步)
4、进程控制
Linux使用的进程间通信方式包括:
1、管道(pipe)和有名管道(FIFO)
2、信号(signal)
3、消息队列
4、共享内存
5、信号量
6、套接字(socket)
一、管道通信
管道是单向的、先进先出,它把一个进程的输出和另一个进程的输入连接在一起。写进程在管道的尾部写入数据,读进程在管道的头部读出数据。
管道分为两种:无名管道和有名管道,前者是用于父子进程之间通信,后者可以用于任意两个进程之间通信。
无名管道由pipe()函数创建
int pipe(int filedis[2]);
当一个管道建立时,它会创建两个文件描述符:filedis[0]用于读管道(管道头部),filedis[1]用于写管道(管道尾部)
管道关闭
int close(int fd);
管道读写
管道用于不同进程间的通信。通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道。
注意:必须在系统调用fork()前调用pipe(),否则子进程将不会继承文件描述符。如果pipe()在fork()之后调用会创建两个管道。父子进程要互相通信必须使用同一个管道。
创建管道之后会创建两个文件描述符filedis[0]、filedis[1],然后就可以用read、write系统调用读写管道。
例如:
read(filedis[0], buf_r, sizeof(buf_r); //读管道
write(filedis[1], "hello world", 12); //写管道
步骤:首先创建管道,然后利用fork创建子进程(子进程会继承父进程创建的管道的文件描述符,此时两个进程都使用同一个管道),利用文件操作的读(read)写(write)函数对管道进行读写即可。
命名管道(FIFO)
命名管道和无名管道的不同点在于无名管道只能父子进程之间通信,但是命名管道是可以在同一个操作系统之间任意进程通信。
命名管道的创建(实际上命名管道就是一个文件)
int mkfifo(const char *pathname, mode_t mode)
*pathname: FIFO文件名
*mo de: 属性
创建了一个FIFO(命名管道),相当于creat创建了一个文件就可以用一般的文件访问的函数(open、write、read、close等)使用FIFO
当打开FIFO时,非阻塞标志(O_NONBLOCK)
1、没有使用O_NONBLOCK:访问要求无法满足时,进程将阻塞。如试图读取空的FIFO,将导致进程阻塞。
2、使用了O_NONBLOCK:访问要求无法满足时不阻塞,立刻出错返回,errno是ENXIO
一般步骤:
先定一个文件“/tmp/myfifo"
先在一个进程中用mkfifo创建一个命名管道(FIFO)
再用open打开FIFO
再用write、read系统调用读写这个FIFO
在第二个进程中也打开文件“/tmp/myfifo"
然后在调用write、read系统调用去读写这个FIFO,当读完这个管道之后,里面的数据就被取走了管道就为空了。
像一个管子一样,往里面放东西,然后取走之后就没了。
二、信号通信
如:进程用kill函数将信号发送给另外一个进程,用户也可用kill命令将信号发送给其他进程
信号类型
常见的信号类型
信号的处理方式
1、忽略此信号
有两种信号是不能忽略的:一种是SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是用来终止和停止进程的
2、执行用户希望的动作
通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户希望的处理
3、执行系统的默认操作
对大多数信号的系统默认动作是终止该进程
信号的放函数有:kill和raise
区别:kill既可以向自身进程发送信号,也可以向其他进程发送新型号。
raise函数是向进程自身发送信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);
pause函数使调用进程挂起直至捕捉到一个信号(然后去信号处理函数中处理)
#include <unistd.h>
int pause(void)
只有执行了一个信号处理函数后,挂起才结束。
三、共享内存
共享内存是被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法。
共享内存实现分为两个步骤:
一、创建共享内存,使用shmget函数。
二、映射共享内存,将这段共享内存映射到具体的进程空间去,使用shmat函数
/*正确函数*/
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
四、消息队列
信号能够传送信息量有限,管道只能传送无格式的字节流。
消息队列就是一个消息链表。可以把消息看作一个记录,具有特定的格式。进程可以向消息队列中按照一定的规则添加新消息(进队列)。另外一些进程可以从消息队列中读走消息(出队列)。
消息队列键值(每个消息队列都有一个唯一的键值),要想获得一个消息队列的描述字,必须提供该消息队列的键值。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
五、信号量
信号量与其他进程间通信方式不大相同,主要用途是保护临界资源。进程可以根据它判断是否能够访问某些共享资源。除了用于访问控制外,还可以用于进程同步。
临界资源:在多任务环境中,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。例如:打印机
信号量是实质就是一个整数(初始值为1表示临界资源现在可用)
给临界资源添加一个信号量,假设有两个进程A和B都要用这个临界资源,那么在访问这个临界资源之前,要先获取这个临界资源的信号量。检测信号量的值是否大于0,如果是大于0的就将信号量减一,然后就访问临界资源。
假设现在A进程要访问临界资源S,那么A进程先获取临界资源S的信号量M并判断M的值是否大于0,如果大于0(临界资源可访问)那么就将信号量M减一,然后就访问这个临界资源。此时,进程B也想访问这个临界资源S,那么进程B也要先获取临界资源S的信号量M,此时M为0(表示临界资源不可访问),那么B进程就阻塞(直至A进程释放临界资源S(信号量M加一)才唤醒B进程),
信号量分类:二值信号量(只能取0或1)、任意时刻只允许一个进程访问临界资源
计数信号量(任意非负值)、如果信号量大于1,可以允许多个进程访问临界资源