Linux系统编程(七)进程间通信IPC

进程间通讯的7种方式_进程间通信的几种方法-CSDN博客

  1. 管道 pipe(命名管道和匿名管道);
  2. 信号 signal;
  3. 共享内存;
  4. 消息队列;
  5. 信号量 semaphore;
  6. 套接字 socket;

1. 管道

内核提供,单工,自同步机制。 

1.1 匿名管道

磁盘上无法看到,只能有亲缘关系的进程才能用匿名管道。一般用于父子进程间通信。

pipe(2) 系统调用可以创建一个匿名管道 pipefd,文件描述符 pipefd[0] 为读管道,pipefd[1] 为写管道。  

#include <unistd.h>

int pipe(int pipefd[2]);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int pipe2(int pipefd[2], int flags);

例子,父进程通过管道发送 hello 给子进程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
  pid_t pid;
  int pipefd[2];
  if (pipe(pipefd) < 0) {
    perror("pipe");
    exit(1);
  }

  pid = fork();

  if (pid > 0) {
    // parent
    close(pipefd[0]);
    write(pipefd[1], "hello", 5);
    close(pipefd[1]);
    wait(NULL);
    exit(0);
  }
  else if (pid == 0) {
    // child
    close(pipefd[1]);
    char buf[50];
    int len = read(pipefd[0], buf, 50);
    printf("%d\n", len);
    write(1, buf, len);
    close(pipefd[0]);
    exit(0);
  }
  else {
    perror("fork");
    exit(1);
  }
  exit(0);
}

1.2 命名管道

磁盘上能看到,文件类型为 p 的文件。

mkfifo(3) 函数可以创建一个 fifo 特殊文件(命名管道)。

#include <sys/types.h>
#include <sys/stat.h>

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

2 XSI -> SysV

2.1 消息队列,message queue 

主动端,先发包的一方;被动端,先收包的一方;消息队列可以用于没有亲缘关系的进程间通信

例子,proto.h,包含了传递的内容:

#ifndef PROTO_H__
#define PROTO_H__

#define KEYPATH "/etc/services"
#define KEYPROJ 'g'

#define NAMESIZE 32

struct msg_st
{
  long mtype;
  char name[NAMESIZE];
  int math;
  int chinese;
};

#endif

receiver.c,接收者:

#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main()
{
  key_t key;
  int msgid;
  struct msg_st rbuf;

  key = ftok(KEYPATH, KEYPROJ);
  if (key < 0) {
    perror("ftok");
    exit(1);
  }

  msgid = msgget(key, IPC_CREAT | 0600);
  if (msgid < 0) {
    perror("msgget");
    exit(1);
  }

  while (1)
  {
    if (msgrcv(msgid, &rbuf, sizeof(rbuf) - sizeof(long), 0, 0) < 0)
    {
      perror("msgrcv");
      exit(1);
    }
    printf("NAME = %s\n", rbuf.name);
    printf("MATH = %d\n", rbuf.math);
    printf("CHINESE = %d\n", rbuf.chinese);
  }

  msgctl(msgid, IPC_RMID, NULL);

  exit(0);
}

运行接收者可以看到创建的 msg queue: 

sender.c,发送者: 

#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

int main()
{
  key_t key;
  struct msg_st sbuf;
  int msgid;

  key = ftok(KEYPATH, KEYPROJ);
  if (key < 0) {
    perror("ftock");
    exit(1);
  }

  msgid = msgget(key, 0);
  if (msgid < 0)
  {
    perror("msgget");
    exit(1);
  }

  sbuf.mtype = 1;
  strcpy(sbuf.name, "lzp");
  sbuf.math = 99;
  sbuf.chinese = 100;
  if (msgsnd(msgid, &sbuf, sizeof(sbuf) - sizeof(long), 0) < 0) {
    perror("msgsend");
    exit(1);
  }

  puts("ok!");

  exit(0);
}

先运行 receiver,然后运行 sender 发送信息: 

2.2 信号量,semaphore arrays

semget、semop、semctl;可以用于没有亲缘关系的进程间通信

2.3 共享内存,shared memory segment

shmget、shmop、shmctl;之前可以通过 mmap 实现共享内存,不过只能在有亲缘关系的进程间通信。匿名 ipc 也只能在有亲缘关系的进程间通信;

3. 网络套接字 socket

3.1 跨主机的传输

  • 字节序大端存储(低地址处放高字节)小端存储(低地址处放低字节,x86);主机字节序(host)和网络字节序(network);主机序转网络序,网络序转主机序;htons,htonl,ntohs,ntohl。
  • 对齐:结构体中 char 类型可能会占 4 个字节;如果不对齐,那么 int 类型的存储地址可能就不是在 4 的倍数的地址上了,可能需要两次访存才能取出一个 int 类型。深入理解字节对齐-CSDN博客
  • 类型长度问题:不同主机间的架构,机器字长可能不一样,结构体类型大小也可能不一样。解决方案(int32_t,int16_t ...);

socket(2) 系统调用如下: 

#include <sys/types.h>    
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • domain 指定协议族;
    • AF_INET      IPv4 Internet protocols    ip(7)
    • AF_INET6     IPv6 Internet protocols   ipv6(7)
  • type 指定通信语义;
    • SOCK_STREAM(流式传输):Provides sequenced, reliable, two-way, connection-based byte streams.
    • SOCK_DGRAM(报式传输)  :Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
  • protocol 指定协议族中的协议;

bind(2) 系统调用可以给 socket 绑定地址。

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

recvfrom(2) 可以从 socket 上接收一条 message: 

#include <sys/types.h>
#include <sys/socket.h>

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

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  1. recv(2) 是用于流式套接字的,是提前建立好连接的,一对一的,点对点的, 不需要记录对方的 socket 地址;
  2. recvfrom(2) 可以用于报式和流式套接字,每次需要传入需要通信的 socket 地址;

sendto(2) 函数可以发送 msg 到对应的 socket 地址,可以用于流式和报式传输;而 send(2) 函数不需要指定 socket 地址,只能用于事先建立好连接的流式传输。

#include <sys/types.h>
#include <sys/socket.h>

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

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

3.2 报式套接字(udp)

被动端(先运行)

  1. 取得 socket(socket);
  2. 给 socket 取得地址,相当于绑定本地的地址(bind);
  3. 收/发消息(recvfrom);
  4. 关闭 socket(close);

主动端

  1. 取得 socket;
  2. 给 socket 取得地址(可省略);
  3. 收/发消息;
  4. 关闭 socket;

__attribute__((packed)):packed属性:使用该属性可以使得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐。即不进行对齐。

使用如下指令查看被动段的地址和端口(u代表udp):

netstat -anu

proto.h 约定传输格式和内容: 

#ifndef PROTO_H__
#define PROTO_H__

#define RCVPORT 1989

#define NAMESIZE  11

// communication struct
struct msg_st
{
        char name[NAMESIZE];
        int math;
        int ch;
} __attribute__((packed));




#endif

receiver.c 接收方, 注意多字节需要使用 ntohl() 来转换:

#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define IPSTRSIZE 40

int main()
{
        int sd; // socket fd
        struct sockaddr_in laddr;
        struct sockaddr_in raddr;
        struct msg_st rbuf;
        socklen_t raddr_len;
        char ipstr[IPSTRSIZE];

        // IPV4 DGRAM UDP
        sd = socket(AF_INET, SOCK_DGRAM, 0/* IPPROTO_UDP */);
        if (sd < 0) {
                perror("socket");
                exit(1);
        }

        laddr.sin_family = AF_INET;
        laddr.sin_port = htons(RCVPORT);
        inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);
        if (bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
                perror("bindqqq");
                exit(1);
        }

        /* !!! */
        raddr_len = sizeof(raddr);

        while (1)
        {
                recvfrom(sd, &rbuf, sizeof(rbuf), 0, (struct sockaddr *)&raddr, &raddr_len);
                inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
                printf("message from %s:%d---\n", ipstr, ntohs(raddr.sin_port));
                printf("name = %s\n", rbuf.name);
                printf("name = %d\n", ntohl(rbuf.math));
                printf("name = %d\n", ntohl(rbuf.ch));
        }
        close(sd);

        exit(0);
}

sender.c 发送方,发送报文给对应的地址: 

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include "proto.h"
#include <unistd.h>

int main(int argc, char *argv[])
{
        int sd;
        struct msg_st sbuf;
        struct sockaddr_in raddr;

        if (argc < 2)
        {
                fprintf(stderr, "usage..\n");
                exit(1);
        }

        sd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sd < 0)
        {
                perror("socket");
                exit(1);
        }

        //bind();
        strcpy(sbuf.name, "Alan");
        sbuf.math = htonl(99);
        sbuf.ch = htonl(93);

        raddr.sin_family = AF_INET;
        raddr.sin_port = htons(RCVPORT);
        inet_pton(AF_INET, argv[1], &raddr.sin_addr);

        if (sendto(sd, &sbuf, sizeof(sbuf), 0, (struct sockaddr *)&raddr, sizeof(raddr)) < 0)
        {
                perror("sendto");
                exit(1);
        }

        puts("ok\n");

        close(sd);

        exit(0);
}

运行结果: 

报式套接字还能实现多播、广播(全网广播和子网广播)、组播:

可以通过 getsockopt() 和 setsockopt() 来打开广播选项。然后将发送的目标地址改成 255.255.255.255 就可以发送广播了。

多点通信(广播、多播、组播)只能用报式套接字实现,因为流式套接字是一对一的,点对点的。

udp 会出现丢包的问题,TTL生存周期(路由跳转个数) ,丢包是由阻塞造成的,当等待队列快满的时候会发生丢包(网络太拥塞),可以使用流量控制解决(限制发送端的速率)。

3.3 流式套接字(tcp)

协议,约定双方对话的格式。

三次握手容易被ddos攻击,可以去掉半连接池,然后使用cookie(对方的ip+端口加上我放的ip+端口加上一个内核产生的令牌)。

Client 端和 Server 端:

client:

  1. 获取 socket;
  2. 给 socket 取得地址;
  3. 发送连接;
  4. 收/发消息;
  5. 关闭连接;

server:

  1. 获取 socket;
  2. 给 socket 取得地址;
  3. 将 socket 置为监听模式;
  4. 接受连接;
  5. 收/发消息;
  6. 关闭;

服务端(server.c)

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <time.h>
#include "proto.h"

#define IPSTRSIZE 40
#define BUFSIZE 1024

static void server_job(int sd)
{
        char buf[BUFSIZE];
        int len = sprintf(buf, FMT_STAMP, (long long)time(NULL));
        if (send(sd, buf, len, 0) < 0) {
                perror("send()");
                exit(1);
        }
}

int main()
{
        struct sockaddr_in laddr, raddr;
        socklen_t raddr_len;
        char ipstr[IPSTRSIZE];
        int sd = socket(AF_INET, SOCK_STREAM, 0/* default IPPROTO_TCP */);
        if (sd < 0) {
                perror("socket()");
                exit(1);
        }

        laddr.sin_family = AF_INET;
        laddr.sin_port = htons(atoi(SERVERPORT));
        inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);

        if (bind(sd, (void *)&laddr, sizeof(laddr)) < 0) {
                perror("bind()");
                exit(1);
        }
        // listen for connections on a socket
        if (listen(sd, 200) < 0)
        {
                perror("listen()");
                exit(1);
        }

        raddr_len = sizeof(raddr);
        while (1) {
                // accept a connection on a socket
                int newsd;
                if ((newsd = accept(sd, (void *)&raddr, &raddr_len)) < 0) {
                        perror("accept()");
                        exit(1);
                }

                inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
                printf("Client: %s : %d\n", ipstr, ntohs(raddr.sin_port));

                server_job(newsd);
                // close newsd
                close(newsd);
        }

        close(sd);

        exit(0);
}

运行后使用 netstat -ant 可以查看:

当连接被释放后,服务端会进入一段时间的 timewait 状态。

客户端(client.c):

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "proto.h"
int main(int argc, char* argv[])
{
        struct sockaddr_in raddr;
        long long stamp;
        if (argc < 2) {
                fprintf(stderr, "Usage...\n");
                exit(1);
        }

        int sd = socket(AF_INET, SOCK_STREAM, 0);
        if (sd < 0) {
                perror("socket()");
                exit(1);
        }

        // initiate a connection on a socket
        raddr.sin_family = AF_INET;
        raddr.sin_port = htons(atoi(SERVERPORT));
        inet_pton(AF_INET, argv[1], &raddr.sin_addr);
        if (connect(sd, (void *)&raddr, sizeof(raddr)) < 0) {
                perror("connect()");
                exit(1);
        }

        FILE* fp = fdopen(sd, "r+");
        if (fp == NULL) {
                perror("fdopen()");
                exit(1);
        }

        if (fscanf(fp, FMT_STAMP, &stamp) < 1) {
                fprintf(stderr, "bad format\n");
        }
        else {
                fprintf(stdout, "stamp = %lld\n", stamp);
        }

        fclose(fp);
        exit(0);
}

上面这种方法有一个缺点,就是假如 server_job 的任务执行时间太长的话,需要等待 server_job 执行完后才能继续 accept 下一个连接请求,这样效率十分低,所以可以采用多线程的方式来解决,每个进程或线程处理一个连接请求。

  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值