4 进程间通信(三)

4.5 进程通信之信号通信

4.5.1 信号机制

  1. 一般地,中断主要是指硬件中断,比如GPIO中断、定时器中断,这些硬件中断时外设模块工作时,发送给CPU的,也是一种异步方式。

  2. 信号是软件层次上对中断机制的一种模拟,是一种异步通信方式。

  3. Linux内核通过信号通知用户进程,不同的信号类型代表不同的事件。

  4. Linux对早期的unix信号机制进行了扩展。

4.5.2 常见信号类型

4.5.3 信号发送函数

kill函数

头文件  #include <unistd.h>

       #include <signal.h>

函数原型int kill(pid_t pid, int sig);

参数  pid : 指定接收进程的进程号

            0代表同组进程;-1代表所有除了INIT进程和当前进程之外的进程

     sig : 信号类型

返回值 成功返回0,失败返回EOF

raise函数

头文件  #include <unistd.h>

       #include <signal.h>

函数原型int raise(int sig);

参数  sig : 信号类型

返回值 成功返回0,失败返回EOF

alarm函数

头文件  #include <unistd.h>

       #include <signal.h>

函数原型 int alarm(unsigned int seconds);

参数  seconds 定时器的时间

返回值 成功返回上个定时器的剩余时间,失败返回EOF

pause函数

进程调用这个函数后会一直阻塞,直到而被信号中断,功能和sleep类似。

头文件  #include <unistd.h>

       #include <signal.h>

函数原型 int pause(void);

返回值  成功返回0,失败返回EOF

signal函数

设置信号响应方式,请注意这个函数和kill、killall的区别,我们中文使用者会理解为发信号,实际上它并不是发信号。

头文件  #include <unistd.h>

       #include <signal.h>

函数原型 void (*signal(int signo, void(*handler)(int)))(int)

参数  signo 要设置的信号类型

     handler 指定的信号处理函数;

返回值 成功返回0,失败返回EOF

4.5.4 进程捕捉信号

程序源码(参考jz2440\process_single\send_single.c)

01 /**********************************************************************
02  * 功能描述: 1.捕捉终端发送过来的信号
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <signal.h>
16 
17 void handler(int signo)
18 {
19  	switch (signo) {
20 		case SIGINT:
21 			printf("I have got SIGINT\n");
22 			break;
23 			
24 		case SIGQUIT:
25 			printf("I have got SIGQUIT\n");
26 			break;
27 			
28 		default:
29 			printf("don't respond to this signal[%d]\n", signo);
30 			exit(0);
31 	}
32 }
33 
34 int main(int argc, char *argv[])
35 {
36 	signal(SIGINT, handler);
37 	signal(SIGQUIT, handler);
38 	while (1)
39 		pause();
40 	return 0;
41 }

JZ2440实验

  • 编译
arm-linux-gcc send_single.c -o send_single
  • 拷贝到NFS文件系统
cp send_single /work/nfs_root/first_fs
  • 运行
./send_single

​ 实际上在利用共享内存实现进程间通信时,我们已经使用到了信号通信,父进程写完共享内存后发送信号通知子进程,子进程收到信号后开始读共享内存,这里就不在给出两个进程之间使用信号通信的例子了,请读者参考共享内存中实现两个进程通信的代码。

4.6 进程通信之socket通信

4.6.1 什么是socket

​ 先思考一个问题:网络环境中的进程如何实现通信?比如当我们使用QQ和好友聊天的时候,QQ进程是如何与服务器以及你好友所在的QQ进程之间通信的?这些靠的就是socket来实现的。

​ Socket起源于UNIX,Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。在《有名管道》那一节中,我们知道socket也是一种文件类型,只不过socket是一种伪文件,存在于内核缓冲区中,大小不变,一直是0。

​ socket文件一定是成对出现的,server端有一个套接字文件,client端也有一个套接字文件,每个进程需要和对应的套接字文件绑定,每个进程通过读写它的套接字文件,交由内核实现,如下所示。


​ 一般地,socket用来实现网络环境中,不同主机上的进程通信,但是也可以用来在同一个主机上的不同进程之间通信,本小节主要探讨socket用在同一个主机上的不同进程间通信。

4.6.2 相关函数

socket函数

创建socket文件描述符函数

头文件#include <sys/types.h>

     #include <sys/socket.h>

  原型: int socket(int domain, int type, int protocol)

  返回值: 成功,消息队列ID,出错 -1

bind函数

将socket文件描述符和某个socket文件绑定

头文件#include <sys/types.h>

     #include <sys/socket.h>

原型: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数: sockfd:利用系统调用socket()建立的套接字描述符

      addr:代表需要绑定的本地地址信息
     
      addrlen: 本地地址信息长度

返回值: 成功,消息队列ID,出错 -1

listen函数

设置监听某个socket文件描述符,设置能够同时和服务端连接的客户端数量,一般只有server会调用

头文件#include <sys/types.h>

     #include <sys/socket.h>

原型: int listen(int sockfd, int backlog);

参数: sockfd:利用系统调用socket()建立的套接字描述符

      backlog:server可以接受连接的最大client数量

返回值: 成功,消息队列ID,出错 -1

accept函数

等待client建立连接的函数,一般只有server会调用

头文件#include <sys/types.h>

     #include <sys/socket.h>

原型: int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

参数: sockfd:利用系统调用socket()建立的套接字描述符

      addr:指向已经建立连接的对端client地址信息的指针

      addrlen: 对端client地址信息长度

返回值: 成功,消息队列ID,出错 -1

connet函数

client主动连接server函数,一般只有client才会调用

头文件#include <sys/types.h>

     #include <sys/socket.h>

原型: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数: sockfd:利用系统调用socket()建立的套接字描述符

      addr:指向已经建立连接的对端server地址信息的指针

      addrlen: 对端server地址信息长度

返回值: 成功,消息队列ID,出错 -1

send函数

发送数据函数

头文件#include <sys/types.h>

     #include <sys/socket.h>

原型: ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数: sockfd:指向要发送数据的socket文件描述符,已经建立连接的文件描述符

      buf: 存放要发送数据的缓冲区

      len: 实际要发送数据的字节数

      flags: 一般为0或者如下的宏

			MSG_DONTROUTE  绕过路由表查找

			MSG_DONTWAIT   仅本操作非阻塞

			MSG_OOB      发送或接收带外数据

			MSG_PEEK      窥看外来消息

			MSG_WAITALL    等待所有数据

返回值: 成功,消息队列ID,出错 -1

recv函数

接收数据函数

头文件#include <sys/types.h>

     #include <sys/socket.h>

原型: ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数: sockfd:已经建立连接的文件描述符

	  buf: 存放要接收数据的缓冲区

	  len: 实际要接收数据的字节数

	  flags:一般为0或者如下的宏

          MSG_DONTROUTE  绕过路由表查找

          MSG_DONTWAIT   仅本操作非阻塞

          MSG_OOB      发送或接收带外数据

          MSG_PEEK      窥看外来消息

          MSG_WAITALL    等待所有数据

返回值: 成功,消息队列ID,出错 -1

4.6.3 socket实现进程间通信

程序实现一般步骤

Server端

1.创建socket

2.绑定socket

3.设置监听

4.等待客户端连接

5.发送/接收数据

Client端

1.创建socket

2.绑定socket

3.连接

4.发送/接收数据

server源码(参考jz2440\process_socket\server.c)

01 /**********************************************************************
02  * 功能描述: 1.server打印client发送过来的字符串,并将该字符串回发给client
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/stat.h>
16 #include <string.h>
17 #include <arpa/inet.h>
18 #include <sys/un.h>
19 
20 int main(int argc, char *argv[])
21 {
22     int lfd ,ret, cfd;
23     struct sockaddr_un serv, client;
24     socklen_t len = sizeof(client);
25     char buf[1024] = {0};
26     int recvlen;
27 
28     //创建socket
29     lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
30     if (lfd == -1) {
31         perror("socket error");
32         return -1;
33     }
34 
35     //如果套接字文件存在,删除套接字文件
36     unlink("server.sock");
37 
38     //初始化server信息
39     serv.sun_family = AF_LOCAL;
40     strcpy(serv.sun_path, "server.sock");
41 
42     //绑定
43     ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
44     if (ret == -1) {
45         perror("bind error");
46         return -1;
47     }
48 
49     //设置监听,设置能够同时和服务端连接的客户端数量
50     ret = listen(lfd, 36);
51     if (ret == -1) {
52         perror("listen error");
53         return -1;
54     }
55 
56     //等待客户端连接
57     cfd = accept(lfd, (struct sockaddr *)&client, &len);
58     if (cfd == -1) {
59         perror("accept error");
60         return -1;
61     }
62     printf("=====client bind file:%s\n", client.sun_path);
63     
64     while (1) {
65         recvlen = recv(cfd, buf, sizeof(buf), 0);
66         if (recvlen == -1) {
67             perror("recv error");
68             return -1;
69         } else if (recvlen == 0) {
70             printf("client disconnet...\n");
71             close(cfd);
72             break;
73         } else {
74             printf("server recv buf: %s\n", buf);
75             send(cfd, buf, recvlen, 0);
76         }
77     }
78     
79     close(cfd);
80     close(lfd);
81     return 0;
82 }

client源码(参考jz2440\process_socket\client.c)

01 /**********************************************************************
02  * 功能描述: 1.client从标准输入获取到一个字符串,然后将这个字符串发送给server
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/stat.h>
16 #include <string.h>
17 #include <arpa/inet.h>
18 #include <sys/un.h>
19 
20 int main(int argc, char *argv[])
21 {
22     int lfd ,ret;
23     struct sockaddr_un serv, client;
24     socklen_t len = sizeof(client);
25     char buf[1024] = {0};
26     int recvlen;
27 
28     //创建socket
29     lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
30     if (lfd == -1) {
31         perror("socket error");
32         return -1;
33     }
34 
35     //如果套接字文件存在,删除套接字文件
36     unlink("client.sock");
37 
38     //给客户端绑定一个套接字文件
39     client.sun_family = AF_LOCAL;
40     strcpy(client.sun_path, "client.sock");
41     ret = bind(lfd, (struct sockaddr *)&client, sizeof(client));
42     if (ret == -1) {
43         perror("bind error");
44         return -1;
45     }
46 
47     //初始化server信息
48     serv.sun_family = AF_LOCAL;
49     strcpy(serv.sun_path, "server.sock");
50     //连接
51     connect(lfd, (struct sockaddr *)&serv, sizeof(serv));
52 
53     while (1) {
54         fgets(buf, sizeof(buf), stdin);
55         send(lfd, buf, strlen(buf)+1, 0);
56 
57         recv(lfd, buf, sizeof(buf), 0);
58         printf("client recv buf: %s\n", buf);
59     }
60 
61     close(lfd);
62     return 0;
63 }

JZ2440实验

  • 编译
arm-linux-gcc server.c -o server

arm-linux-gcc client.c -o client
  • 拷贝到NFS文件系统
cp server /work/nfs_root/first_fs

cp client /work/nfs_root/first_fs
  • 运行

为方便看程序运行结果,server在后台执行;client在前台运行,client能够接收来自终端的输入。

./server &

./client

image

4.6.4 一个server和多个client之间的通信

​ 在实际项目开发中, 更常见的一种场景是:一个server要和多个client之间通信,这部分实现方式交给读者自行实现,实现的方式有很多,比如如下两种方式,当然还要其他方法。

  1. 多进程实现,一个主进程用来实现检测client的连接,每检测一次client连接,则为这个client创建一个专门的进程,用于实现两者间通信。

  2. 也可以使用多线程实现,一个主线程用来检测client的连接,每检测一次client连接,则为这个client创建一个专门的线程,用于实现两者间通信。