进程间通信总结

概述 :

每个进程拥有独立进程空间的好处。

对于编程人员来说,系统更容易捕获随意的内存读取和写入操作

对于用户来说,操作系统将变得更加健壮,因为一个应用程序无法破坏另一个进程或操作系统的运行(防止被攻击)

独立进程空间的缺点

多任务实现开销较大

编写能够与其他进程进行通信,或者能够对其他进程进行操作的应用程序将要困难得多

广义上的进程间通信

A进程——————文件———————B进程

A进程—————数据库——————B进程

狭义上的真正的 “进程间通信”

管道

信号

消息队列

共享内存

信号量

套接字

进程间通信的原理

尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有进程所共享的,那就是OS,因为甭管运行有多少个进程,但是它们共用OS只有一个。既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方OS来实现数据的转发。因此进程间通信的原理就是,OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数据的转发,达到数据共享的目的。

信号

信号是一种向进程发送通知,告诉其某件事情发生了的一种简单通信机制

信号的产生

另一个进程发送信号

内核发送信号

底层硬件发送信号

常用信号

SIGABRT 6 终止进程,调abort函数是产生 终止,产生core文件

SIGALRM 14 超时,调用alarm函数时产生 终止

SIGBUS 7 硬件故障终止,产生core文件

SIGCHLD 17 子进程状态改变忽略

SIGINT 2 终止进程(ctrl+c)终止

SIGIO 29 异步通知信号 终止

SIGKILL 9 无条件终止一个进程,不可以被捕获或忽略终止

SIGPIPE 13 写没有读权限的管道文件时终止

SIGPOLL 8 轮询事件,涉及POLL机制终止

SIGQUIT 3 终止进程(ctrl+\)终止,产生core文件

SIGSEGV 11 无效存储访问(指针错误)终止,产生core文件

SIGTERM 15 终止,kill PID时,默认发送的就是这个信号 终止

SIGUSR1 10 用户自定义信号1终止

SIGUSR2 12 用户自定义信号2 终止

信号发送

int kill(pid_t pid, int sig);

int raise(int sig);

unsigned int alarm(unsigned int seconds);

void abort();

进程挂起pause

pause:只要一直处于休眠状态,表示pause函数一直是调用成功的。

当被信号唤醒后会返回-1,表示失败了,errno的错误号被设置EINTR(表示函数被信号中断)

信号处理的方式

默认处理

忽略

执行用户需要执行的动作(捕获)

信号处理API

函数原型

#include

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

功能:设置某个信号的处理方式。

处理方式可以被设置为忽略,捕获,默认。进程的进程表(task_struct)中会有一个“信号处理方式登记表”,专门用于记录信号的处理方式,调用signal函数设置某个信号的处理方式时,会将信号的处理方式登记到该表中。

每个进程拥有独立的task_struct结构体变量,因而每个进程的“信号处理方式登记表”都是独立的,所以每个进程对信号的处理方式自然也是独立的,互不干扰。

参数

signum:信号编号。

handler:信号处理方式。

sighandler_t是被typedef后的类型,原类型 void (*)(int),这是一个函数指针类型。

sighandler_t handler也有直接写成void (*handler)(int)。

sighandler_t signal(int signum, void (*handler)(int));

忽略:SIG_IGN

默认:SIG_DFL

捕获:填写类型为void (*)(int)的捕获函数的地址,当信号发生时,会自动调用捕获函数来进行相应的处理。

当然这个捕获函数需要我们自己来实现,捕获函数的int参数,用于接收信号编号。 捕获函数也被称为信号处理函数。

void signal_fun1(int signo)

{

...

}

void signal_fun2(int signo)

{

...

}

int main(void)

{

signal(SIGINT, signal_fun1);

signal(SIGSEGV, signal_fun2);

return 0;

}

捕获函数什么时候被调用?

进程接收到信号时就调用,调用时会中断进程的正常运行,当调用完毕后再会返回进程的正常运行。

返回值

成功:返回上一次的处理方式

失败:返回SIG_ERR宏值,并且设置errno。

管道

无名管道

具体来说就是,内核会开辟一个“管道”,通信的进程通过共享这个管道,从而实现通信。

int pipe(int pipefd[2]);

函数原型

int pipe(int pipefd[2]);

功能

创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符关联起来。

参数

缓存地址,缓存用于存放读写管道的文件描述符。从这个参数的样子可以看出,这个缓存就是一个拥有两个元素的int型数组。

元素[0]:里面放的是读管道的读文件描述符

元素[1]:里面放的是写管道的写文件描述符。

特别需要注意的是,这里的读和写文件描述符,是两个不同的文件描述符。

特点

管道只允许具有血缘关系的进程间通信,如父子进程间的通信

管道只允许单向通信

读管道时,如果没有数据的话,读操作会休眠(阻塞),写数据时,缓冲区写满会休眠(阻塞)

数据被读出,数据就会被管道删除

有名管道

管道应用的一个重大限制是它没有名字,只适合具有亲缘性质的进程之间通信。命名管道克服了这种限制,FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。

函数原型

int mkfifo(const char *pathname, mode_t mode);

功能

创建有名管道文件,创建好后便可使用open打开。如果是创建普通文件的话,我们可以使用open的O_CREAT选项来创建,比如:open("./file", O_RDWR|O_CREAT, 0664);

但是对于“有名管道”这种特殊文件,这里只能使用mkfifo函数来创建。

参数

pathname:被创建管道文件的文件路径名。

mode:指定被创建时原始权限,一般为0664(110110100),必须包含读写权限。

返回值:成功返回0,失败则返回-1,并且errno被设置。

有名管道的使用步骤

进程调用mkfifo创建有名管道

open打开有名管道

read/write读写管道进行通信

有名管道的使用注意事项

“有名管道”这种特殊文件,只能使用mkfifo函数来创建

为了保证管道一定被创建,最好是两个进程都包含创建管道的代码,谁先运行就谁先创建,后运行的发现管道已

经创建好了,那就直接open打开使用。

不能以O_RDWR模式打开命名管道FIFO文件,否则其行为是未定义的,管道是单向的,不能同时读写;

特点:

任意两个进程通信

使用一个“有名管道”是无法实现双向通信的,因为也涉及到抢数据的问题

消息队列

消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表称为消息队列。

分类

System V的消息队列

Posix消息队列

两者区别:对Posix消息队列的读总是返回最高优先级的最早消息,对System V消息队列的读则可以返回任意指定优先级的消息

消息的组成

struct msgbuf

{

long mtype; /* 放消息编号,必须> 0 */

char mtext[msgsz]; /* 消息内容(消息正文) */

};

消息编号:识别消息用

消息正文:真正的信息内容

消息队列的使用步骤

使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的标识符(msqID),后续收发消息就是使用这个标识符来实现的。

收发消息

发送消息:

A、进程先封装一个消息包 ,这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文写到结构体的成员中。

struct msgbuf

{

long mtype; /* 放消息编号,必须> 0 */

char mtext[msgsz]; /* 消息内容(消息正文) */

};

B、调用相应的API发送消息

接受消息:

调用API接收消息时,必须传递两个重要的信息

A、消息队列标识符

B、你要接收消息的编号

使用msgctl函数,利用消息队列标识符删除消息队列

消息队列API

int msgget(key_t key, int msgflg);

fork前创建

使用ftok函数,利用与创建者相同的“路径名”和8位整形数,生成相同的key值

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

函数原型:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

功能:

发送消息到消息队列上。 说白了就是将消息挂到消息队列上。

返回值

成功:返回0,

失败:返回-1,errno被设置

参数

msqid:消息队列的标识符。

msgp:存放消息的缓存的地址,类型struct msgbuf类型

这个缓存就是一个消息包(存放消息的结构体变量)。

struct msgbuf

{

long mtype; /* 放消息编号,必须 > 0 */

char mtext[msgsz]; /* 消息内容(消息正文) */

};

msgsz:消息正文大大小。

msgflg:

- 0:阻塞发送消息

也就是说,如果没有发送成功的话,该函数会一直阻塞等,直到发送成功为止。

- IPC_NOWAIT:非阻塞方式发送消息,不管发送成功与否,函数都将返回

也就是说,发送不成功的的话,函数不会阻塞。

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

函数原型

size_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

功能:接收消息

说白了就是从消息队列中取出别人所放的某个编号的消息。

返回值

成功:返回消息正文的字节数

失败:返回-1,errno被设置

参数

msqid:消息队列的标识符。

msgp:缓存地址,缓存用于存放所接收的消息

类型还是struct msgbuf:

struct msgbuf

{

long mtype; /* 存放消息编号*/

char mtext[msgsz]; /*存放 消息正文内容 */

};

msgsz:消息正文的大小

msgtyp:你要接收消息的编号

int msgflg:

- 0:阻塞接收消息

也就是说如果没有消息时,接收回阻塞(休眠)。

- IPC_NOWAIT:非阻塞接收消息

也就是说没有消息时,该函数不阻塞

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msgctl函数原型

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

功能

ctl就是控制contrl的意思,从这个名字我们就能猜出,这个函数的功能是根据cmd指定的要求,去控制消息队列,

参数

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msqid:消息队列标识符

cmd:控制选项,其实cmd有很多选项,我这里只简单介绍三个

- IPC_STAT:将msqid消息队列的属性信息,读到第三个参数所指定的缓存。

- IPC_SET:使用第三个参数中的新设置去修改消息队列的属性

+ 定一个struct msqid_ds buf。

+ 将新的属性信息设置到buf中

+ cmd指定为IPC_SET后,msgctl函数就会使用buf中的新属性去修改消息队列原有的属性。

- IPC_RMID:删除消息队列

删除消息队列时,用不到第三个参数,用不到时设置为NULL。

buf:存放属性信息

有的时候需要给第三个参数,有时不需要,取决于cmd的设置。buf的类型为struct msqid_ds,有关这个结构体类型,这里这里只进行简单了解。

结构体中的成员都是用来存放消息队列的属性信息的。

struct msqid_ds

{

struct ipc_perm msg_perm; /* 消息队列的读写权限和所有者 */

time_t msg_stime; /* 最后一次向队列发送消息的时间*/

time_t msg_rtime; /* 最后一次从消息队列接收消息的时间 */

time_t msg_ctime; /* 消息队列属性最后一次被修改的时间 */

unsigned long __msg_cbytes; /* 队列中当前所有消息总的字节数 */

msgqnum_t msg_qnum; /* 队列中当前消息的条数*/

msglen_t msg_qbytes; /* 队列中允许的最大的总的字节数 */

pid_t msg_lspid; /* 最后一次向队列发送消息的进程PID */

pid_t msg_lrpid; /* 最后一次从队列接受消息的进程PID */

};

struct ipc_perm

{

key_t __key; /* Key supplied to msgget(2):消息队列的key值 */

uid_t uid; /* UID of owner :当前这一刻正在使用消息队列的用户 */

gid_t gid; /* GID of owner :正在使用的用户所在用户组 */

uid_t cuid; /* UID of creator :创建消息队列的用户 */

gid_t cgid; /* GID of creator :创建消息队列的用户所在用户组*/

unsigned short mode; /* Permissions:读写权限(比如0664) */

unsigned short __seq; /* Sequence number :序列号,保障消息队列ID不被立即

重复使用 */

};

控制1:获取消息队列属性

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

当cmd被设置为IPC_STAT时,msgctl将获取消息队列的属性信息,并保存到buf中。

struct msqid_ds buf;

msgctl(msgid, IPC_STAT, &buf);

控制2:删除消息队列

msgctl(msgid, IPC_RMID, NULL);

消息队列的特点

传送有格式的消息流

多进程网状交叉通信时,消息队列是上上之选

能实现大规模数据的通信

共享内存

让同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新。

共享内存的使用步骤

进程调用shmget函数创建新的或获取已有共享内存

进程调用shmat函数,将物理内存映射到自己的进程空间

shmdt函数,取消映射

调用shmctl函数释放开辟的那片物理内存空间

int shmget(key_t key, size_t size, int shmflg);

函数原型:

int shmget(key_t key, size_t size, int shmflg);

功能:创建新的,或者获取已有的共享内

返回值

成功:返回共享内存的标识符,以后续操作

失败:返回-1,并且errno被设置。

参数

key:用于生成共享内存的标识符

可以有三种设置:

IPC_PRIVATE:指定这个后,每次调用shmget时都会创建一个新共享内存。

自己指定一个长整型数

使用ftok函数,通过路径名和一个8位的整形数来生成key值

size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍。一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动帮你补成整数倍。

semflg:与消息队列一样。指定原始权限和IPC_CREAT,比如0664|IPC_CREAT。

只有在创建一个新的共享内存时才会用到,否者不会用到。

void *shmat(int shmid, const void *shmaddr, int shmflg);

函数原型

void *shmat(int shmid, const void *shmaddr, int shmflg);

功能

将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始地址(虚拟地址)。

有了这个地址后,就可以通过这个地址对共享内存进行读写操作。

参数

shmid:共享内存标识符。

shmaddr:指定映射的起始地址

有两种设置方式

自己指定映射的起始地址(虚拟地址)。

我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。

NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。

这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用。

shmflg:指定映射条件。

0:以可读可写的方式映射共享内存

也就是说映射后,可以读、也可以写共享内存。

SHM_RDONLY:以只读方式映射共享内存

也就是说映射后,只能读共享内存,不能写。

返回值

成功:则返回映射地址

失败:返回(void *)-1,并且errno被设置

int shmdt(const void *shmaddr);

函数原型

int shmdt(const void *shmaddr);

功能:取消建立的映射。

返回值:调用成功返回0,失败返回-1,且errno被设置。

参数:shmaddr:映射的起始地址(虚拟地址)。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能:根据cmd的要求,对共享内存进行相应控制。

比如:

获取共享内存的属性信息

修改共享内存的属性信息

删除共享内存

等等

删除共享内存是最常见的控制。

参数

shmid:标识符。

cmd:控制选项

- IPC_STAT:从内核获取共享内存属性信息到第三个参数(应用缓存)。

- IPC_SET:修改共享内存的属性。修改方法与消息队列相同。

- IPC_RMID:删除共享内存,不过前提是只有当所有的映射取消后,才能删除共享内存。 删除时,用不着第三个参数,所以设置为NULL。

共享内存特点

减少进入内核空间的次数

直接使用地址来读写缓存时,效率会更高,适用于大数据量的通信

信号量

当多个进程/线程进行共享操作时,用于资源保护,以防止出现相互干扰的情况

资源保护的操作

互斥

对于互斥操作来说,多进程共享操作时,多个进程间不关心谁先操作、谁后操作的先后顺序问题,它们只关心一件事,那就是我在操作时别人不能操作。

同步

所以所谓同步就是,多个共享操作时,进程必须要有统一操作的步调,按照一定的顺序来操作

信号量

信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥。

分类

二值信号量 :同步和互斥时使用的都是二值信号量。二值信号量的值就两个,0和1,0表示不可以操作,1表示可以操作。 通过对变量进行0、1标记,就可以防止出现相互干扰情况。

多值信号量:信号量的最大值>1,比如为3的话,信号量允许的值为0、1、2、3。

信号量集:信号量其实是一个OS创建的,供相关进程共享的int变量,只不过我们在调用相关API创建信号量时,我们创建的都是一个信号量集合,所谓集合就是可能会包含好多个信号量。 用于互斥时,集合中只包含一个信号量。 用于同步时,集合中会包含多个信号量,至于多少个,需要看情况。

信号量的使用步骤

进程调用semget函数创建新的信号量集合,或者获取已有的信号量集合。

调用semctl函数给集合中的每个信号量设置初始值

调用semop函数,对集合中的信号量进行pv操作(加锁解锁)

P操作(加锁):对信号量的值进行-1,如果信号量的值为0,p操作就会阻塞。

V操作(解锁):对信号量的值进行+1,V操作不存在阻塞的问题

调用semctl删除信号量集合

信号量API

int semget(key_t key, int nsems, int semflg);

函数原型

int semget(key_t key, int nsems, int semflg);

sem就是semaphore的缩写。

功能:根据key值创建新的、或者获取已有的信号量集合,并返回其标识符。

实现互斥时:集合中只需要一个信号量

实现同步时:集合中需要多个信号量

参数

key:设置同消息队列和共享内存。

一般都使用ftok获取key值。· nsems:指定集合中信号量的个数。用于互斥时,数量都指定为1,因为只需要一个信号量。如果是同步的话就需要至多为多个,至于到底是多少个,讲到同步时再说。

semflg:设置同消息队列和共享内存。 一般都设置为0664|IPC_CREAT。

返回值:调用成功则返回信号量集合的标识符,失败则返回-1,并且errno被设置。

int semctl(int semid, int semnum, int cmd, ...);

函数原型

int semctl(int semid, int semnum, int cmd, ...);

功能

根据cmd的要求对集合中的各个信号量进行控制,...表示它是一个变参函数,如果第四个参数用不到的话,可以省略不写。

返回值:调用成功返回非-1值,失败则返回-1,errno被设置。

参数说明

semid:信号量标识符。 通过标识符就能找到信号量集合。

int semop(int semid, struct sembuf *sops, unsigned nsops);

参数

semid:信号量集合的标识符。

sops:这个参数更好理解的写法是struct sembuf sops[],

第三个参数nsops就是用于指定数组元素个数的。

每一个数组成员对应一个信号量,每一个元素都是一个struct sembuf结构体变量,内部成员

决定着你要对集合中哪一个信号量进行操作,要进行的是p操作呢,还是v操作

结构体成员

struct sembuf

{

unsigned short sem_num;

short sem_op;

short sem_flg;

}

这个结构体不需要我们自己定义,因为在semop的头文件中已经定义了。

如果你无法判断这个结构体是否需要我们自己定义,那你就不要定义,如果编译提示这个结构体类型不存在,就说明需要自己定义,编译通过就说明在系统头文件中早就定义好了。

+ sem_num:信号量编号,决定对集合中哪一个信号量进行pv操作

+ sem_op:设置为-1,表示想-1进行p操作,设置1表示想+1进行v操作

+ sem_flg:

IPC_NOWAIT:

一般情况下,当信号量的值为0时进行p操作的话,semop的p操作会阻塞。如果你不想阻塞的话,可以指定这个选项,NOWAIT就是不阻塞的意思。不过除非某些特殊情况,否则我们不需要设置为非阻塞。

SEM_UNDO:防止死锁

还是以二值信号量为例,当进程在v操作之前就结束时,信号量的值就会一直保持为0,那么其它进程将永远无法p操作成功,会使得进程永远休眠下去,这造成就是死锁。

但是设置了SEM_UNDO选项后,如果进程在结束时没有V操作的话,OS会自动帮忙V操作,防止死锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值