linux进程间通信概述

###进程间通信概述
为什么需要进程间通信
(1)进程间通信(IPC)指的是2个任意进程之间的通信
(2)同一个进程在一个地址空间中,所以同一个进程的不同模块(不同函数、不同文件)之间都是很简单的(很多时候都是全局变量、也可以通过函数形参实参传递)
(3)2个不同的进程处于不同的地址空间,因此要互相通信很难
(4)结论:IPC技术在一般中小型程序中用不到,在大型程序中才会用到

###linux内核提供多种进程间通信机制
发现一个IPC函数详解网站: http://www.cnblogs.com/52php/tag/%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1/

(1)无名管道(管道)
1.原理:调用pipe后在内存中的特殊文件,系统分配一个页面作为读写缓冲区
2.方法:父进程创建管道后fork子进程,子进程继承父进程的管道fd,此时父子进程都对管道有两个
pipefd,一个为读端fd[0],另一个为写端fd[1],为了防止一端自己写对方还没来得及读就被自己读掉了内容,所以父子进程需各自关闭一个端
3.限制:只能在亲缘关系的进程间通信、半双工(可建立两个管道实现全双工)、没有名字、缓冲区大小受限
4.常用函数
FILE *popen(const char *command, const char *type);
int pipe(int pipefd[2]);
int pclose(FILE *stream);

(2)有名管道(fifo)
1.原理:实质也是内核维护的一块内存,表现为有路径名与之相关联的特殊设备文件存在于文件系统中
2.方法:使用mkfifo创建fifo文件,然后两个进程根据fifo的文件访问,一个读一个写
3.限制:半双工(注意不限父子进程,任意2个进程都可)
4.函数:
mkfifo
int mkfifo(const char *pathname, mode_t mode);
mode为该文件的权限,umask会影响FIFO文件的权限
注意:O_NONBLOCK标志在FIFO文件中会有一些影响
1.当使用非阻塞标志时,若读操作的进程在对面没有写操作的进程时会立即返回并报错
2.当使用默认的阻塞时,读操作的进程会等待直到会面的写操作进程往文件写内容才返回;同样,写操作的进程会等待对面读操作的进程读取才会返回

(3)SystemV IPC:信号量、消息队列、共享内存
系统通过一些专用API来提供SystemV IPC功能
一、消息队列
(1)本质上存在内核的消息队列,只有重启内核或显式删除才能真正删除消息队列
(2)相关结构体,函数(参考上述网站)
与命名管道相比,消息队列的优势在于:
1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难
2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法
3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收
PS:事实上,它是一种正逐渐被淘汰的通信方式,我们可以用流管道或者套接口的方式来取代它,所以,我们对此方式也不再解释,也建议读者忽略这种方式

二、信号量(互斥,同步)
信号量的原理是数据操作锁的概念,本身不具备数据交换的功能,而是通过控制其他的信号资源来实现进程间通信,相当于内存中的标志;同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。

*一般说来,为了获得共享资源,进程需要执行下列操作: 
(1)测试控制该资源的信号量
(2)若此信号量的值为正,则允许进行使用该资源。进程将信号量减1
(3) 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)
(4)当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程 

*对信号量的值进行增减操作均为原子操作,这是由于信号量主要作用是维护资源的互斥或多进程的同步,但信号量在创建及初始化时不能保证其为原子操作!

*信号量的工作原理
由于信号量只能进行两种操作,即P(sv)(代表等待、关操作)和V(sv)(代表信号、开操作),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

三、共享内存(大量数据的处理)
共享内存是最便捷,最快速的通信方式,将同一块内存分别映射到A,B两个进程的逻辑空间。
得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。
前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的将是 实际的物理内存,
在Linux系统下,这只有通过限制Linux系统存取的内存才可以做到,这当然不太实际。常用的方式是通过shmXXX函数族来实现利 用共享内存进行存储的。 
(1)类似于LCD显示时的显存用法
(2)共享内存通常都需要配合使用,如共享内存和信号量结合,A,B进程分别写读图片,但是它们是异步的无法知道对方是否写完了或者读完了,所以需要一个标志位-信号量

(4)信号
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise,alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
参考: http://blog.chinaunix.net/uid-25002135-id-3300821.html

1.信号是内容受限的一种异步通信机制
信号的目的:用来通信
信号是异步的(对比硬件中断)

信号本质上是int型数字编号(事先定义好的)

2.信号由谁发出
用户在终端按下按键
硬件异常后由操作系统内核发出信号
用户使用kill命令向其他进程发出信号
某种软件条件满足后也会发出信号,如alarm闹钟时间到会产生SIGALARM信号,向一个读端已经关闭的管道write时会产生SIGPIPE信号

3.信号的处理方式
1、  捕捉信号:对于要捕捉的信号,可以为其指定信号处理函数,信号发生时该函数自动被调用,在该函数内部实现对该信号的处理。
2、  忽略信号:大多数信号都可使用这种方式进行处理,但是SIGKILL和SIGSTOP这两个信号不能被忽略,同时这两个信号也不能被捕获和阻塞。此外,如果忽略某些由硬件异常产生的信号(如非法存储访问或除以0),则进程的行为是不可预测的。
3、  按照系统默认方式处理。大部分信号的默认操作是终止进程,且所有的实时信号的默认动作都是终止进程。

4.常见信号介绍 /usr/include/i386-linux-gun/bits/signum.h(/usr/include/signal.h)
(1)SIGINT 2 Ctrl+C时OS送给前台进程组中每个进程(即无法打断后台进程)
(2)SIGABRT 6 调用abort函数,进程异常终止
(3)SIGPOLL SIGIO 8 指示一个异步IO事件,在高级IO中提及
(4)SIGKILL 9 杀死进程的终极办法
(5)SIGSEGV 11 无效存储访问时OS发出该信号
(6)SIGPIPE 13 涉及管道和socket(向关闭了的端读写会返回该信号提示)
(7)SIGALARM 14 涉及alarm函数的实现
(8)SIGTERM 15 kill命令发送的OS默认终止信号
(9)SIGCHLD 17 子进程终止或停止时OS向其父进程发此信号
(10)
SIGUSR1 10 用户自定义信号,作用和意义由应用自己定义
SIGUSR2 12

5.常用函数
###signal函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数绑定一个捕获函数后,捕获到信号会自动执行绑定的捕获函数,并且把信号编号作为传参传给捕获函数,即参数int,当该捕获函数被多个信号绑定时,我们可以通过判断接收到的int参数来决定具体执行上面操作
解析:
sighandler_t为函数指针
signum为上面信号的宏定义
handler即为绑定的函数
返回值出错时为SIG_ERR,成功时返回旧的捕获函数
#define SIG_ERR ((__sighandler_t) -1)           /* Error return.  */
#define SIG_DFL ((__sighandler_t) 0)            /* Default action.  */
#define SIG_IGN ((__sighandler_t) 1)            /* Ignore signal.  */
SIG_DFL、SIG_IGN也可以作为绑定的函数,分别是默认处理和忽略处理
注意:signal在不同版本的Linux有一定的可移植性的问题,尤其当绑定了一个自定义函数,而不是自带函数时(SIG_DFL、SIG_IGN),解决办法是使用sigaction函数

###int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
sigaction比signal好的一点:
sigaction可以一次得到设置新捕获函数和获取旧的捕获函数(其实还可以单独设置新的捕获或者单独只获取旧的捕获函数),而signal函数不能单独获取旧的捕获函数而必须在设置新的捕获函数的同时才获取旧的捕获函数

6.与信号有关的几个函数
alarm和pause函数
1. alarm函数
系统只为每个进程维护一个alarm时钟,下一次的调用会把上一次的覆盖掉
首次调用的返回值为0
下次调用为上一次到现在剩余的时间,并且时间会重置

2. pause函数
pause函数的作用就是让当前进程暂停运行(内核挂起),交出CPU给其他进程去执行(while仍要求CPU调度)
当当前进程进入pause状态后当前进程会表现为“卡住、阻塞住”,要退出pause状态当前进程需要被信号唤醒

3.使用alarm和pause来模拟sleep
首先设定好sigaction函数,捕获SIGALRM信号,绑定操作函数
调用alarm函数定时
调用pause函数进程暂停
时间到后,发出SIGALRM信号,pause被唤醒进程继续,绑定函数执行

(5)Socket域套接字
BS和CS
1、CS架构介绍(client server,客户端服务器架构)
2、BS架构介绍(broswer server,浏览器服务器架构)
客户端服务器架构需要下载特定的软件登录,浏览器服务器架构只需要在浏览器上登录即可

网络编程常用函数
1、int socket(int domain, int type, int protocol);
http://blog.csdn.net/xc_tsao/article/details/44123331
解析:
调用成功返回一个文件描述符,类似于open函数,后续操作都是使用这个网络文件描述符
domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族,如IPv4、IPv6等
type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字TCP)、SOCK——DGRAM(数据包套接字UDP)
protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型

2、int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr));
这里注意const struct sockaddr *addr使用的是sockaddr_in,但后续要强制类型转换,如上

3、int listen(int sockfd, int backlog);
服务器进程希望内核在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接数backlog,一般这个值会小30以内

4、int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
结构体转换如bind一样,注意这里的长度addrlen使用的是socklen_t*指针

5、int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

地址转换函数(只用到struct in_addr结构体)
助记:n二进制,a,p点分十进制(字符串格式)
http://blog.csdn.net/zyy617532750/article/details/58595700
(1)inet_aton、inet_ntoa、inet_addr(只支持IPv4)
1、in_addr_t inet_addr(const char *cp); typedef uint32_t in_addr_t;
2、int inet_aton(const char *cp, struct in_addr *inp);
3、char *inet_ntoa(struct in_addr in);

(2)inet_ntop、inet_pton(支持IPv4和IPv6)
1、const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); 
2、int inet_pton(int family, const char *strptr, void *addrptr);

表示IP地址相关数据结构/usr/include/netinet/in.h
以下函数内部自动将本机转成网络字节序,即大段模式(网络同一使用大端模式)
(从上往下嵌套)
(1)struct sockaddr(编程还是要用sockaddr_in或struct sockaddr_in6) 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充
(2)struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_); //宏定义了一个sin_family
    in_port_t sin_port;             /* Port number.  */
    struct in_addr sin_addr;        /* Internet address.  */注意定义IP地址还要进到s_addr


    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
  };
(3)typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型
(4)struct in_addr
  {
    in_addr_t s_addr;
  };

概念:端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程。端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中
分配方式:(0---65535范围内)
(1)端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1---1023之间的端口号,是由ICANN来管理的;
(2)客户端只需保证该端口号在本机上是惟一的就可以了。客户端口号因存在时间很短暂又称临时端口号;
(3)大多数TCP/IP实现给临时端口号分配1024---5000之间的端口号。大于5000的端口号是为其他服务器预留的。
(4)注意:端口号也要注意网络字节序,即大端模式,使用以下函数转换
uint32_t htonl(uint32_t hostlong); h:主机 n:网络 l:long
uint16_t htons(uint16_t hostshort); h:主机 n:网络 s:short
(5)服务器必须设置端口号,但是客户端可以自动分配

编写服务器server
第1步:先socket打开文件描述符
第2步:bind绑定sockefd和当前电脑的ip地址&端口号
第3步:listen监听端口
第4步:accept阻塞等待客户端接入(accept函数的socklen_t *addrlen所指向的必须为正数,即结构体长度)

编写客户端client
第1步:先socket打开文件描述符
第2步:connect链接服务器

注意:
在server中:
socket返回的fd叫做监听fd,是用来监听客户端的,不能用来和任何客户端进行读写;
accept返回的fd叫做连接fd,用来和连接那端的客户端程序进行读写,即send,recv函数都用acceptfd
在client中:
只有socket的fd,故读写都是使用socketfd

自定义应用层协议第一步:规定发送和接收方法
(1)规定连接建立后由客户端主动向服务器发出1个请求数据包,然后服务器收到数据包后回复客户端一个回应数据包,这就是一个通信回合
(2)整个连接的通信就是由N多个回合组成的
自定义应用层协议第二步:定义数据包格式(如结构体等)

常用应用层协议:http、ftp


关于IPC的几个很棒的博客:

https://www.cnblogs.com/yangang92/p/5679641.html

http://blog.csdn.net/eroswang/article/details/1772350


猜你喜欢

转载自blog.csdn.net/KeyonWho/article/details/79517697
今日推荐