linux 中 unix socket 常见使用场景

unix socket 又称本地套接字,用于系统内的进程间通信,不能用于跨系统的网络通信(跨系统网络通信可以使用 tcp, udp)。unix socket 分为 3 类:socketpair, fs unix socket, abstract unix socket。

1 unix socket 分类

unix socket 分类

特点

socket pair

(1)匿名套接字,可用于父子进程间通信

匿名的当然只能用于父子进程之间的通信。

共享内存,管道等这些进程间通信机制,均有这个规律:使用匿名对象进行父子进程间通信,使用文件系统中存在的一个文件对应的 fd 用于无父子关系的进程间通信。

(2)创建一个套接字对,fd[0] 和 fd[1],a 进程通过 fd[0] 发送的数据,b 进程可以通过 fd[1] 接收,b 进程通过 fd[1] 发送的数据, a 进程可以通过 fd[0] 接收。

fs unix socket

(1)会和磁盘上的一个文件绑定,如果磁盘上原来没有这个文件,那么会创建文件

(2)进程退出时,不会自动销毁,对于服务端来说,每次 bind() 之前需要通过 unlink() 删除旧的文件

abstract unix socket

(1) sun_path[0] 是 '\0',即以 '\0' 开头

(2)进程退出时,会自动销毁

unix socket 套接字地址:

unix socket 使用的 addr 类型如下,unix socket 不占用端口号,使用一个字符串类型的 path 来表示套接字的地址。 

struct sockaddr_un {
   sa_family_t sun_family;               /* AF_UNIX */
   char        sun_path[108];            /* Pathname */
};

在 linux 中,不同的套接字有不同的地址类型,ipv4, ipv6, unix socket,地址类型是不同的,分别使用不同的结构体,分别是 struct sockaddr_in, struct sockaddr_in6 以及 struct sockaddr_un,3 种结构体的大小是不一样的。

#include <stdio.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main() {
  printf("sizeof(struct sockaddr): %lu\n", sizeof(struct sockaddr));
  printf("sizeof(struct sockaddr_in): %lu\n", sizeof(struct sockaddr_in));
  printf("sizeof(struct sockaddr_in6): %lu\n", sizeof(struct sockaddr_in6));
  printf("sizeof(struct sockaddr_un): %lu\n", sizeof(struct sockaddr_un));
  printf("sizeof(struct sockaddr_storage): %lu\n", sizeof(struct sockaddr_storage));
  return 0;
}

 另外有两个套接字地址是通用套接字地址,struct sockaddr 和 struct sockaddr_storage,后者的大小是 128,能保存所有的套接字类型。在使用 tcp 或者 unix socket 套接字的时候,bind() 函数传入的地址都要转化成 struct sockaddr * 类型,其中 struct sockaddr_in 和 struct sockaddr 大小相等,struct sockaddr_un 比 struct sockaddr 大,在内核中并不是把套接字地址中的数据拷贝到了 struct sockaddr 中,而是拷贝到 struct sockaddr_storage 中。

bind() 函数系统调用过程如下代码中所示,最终在函数 move_addr_to_kernel() 中将用户的套接字地址拷贝到了 struct sockaddr_strorage 中。

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    return __sys_bind(fd, umyaddr, addrlen);
}

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
    struct sockaddr_storage address;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        ...
    }
    return err;
}

int move_addr_to_kernel(void __user *uaddr, int ulen, struct sockaddr_storage *kaddr)
{
    if (ulen < 0 || ulen > sizeof(struct sockaddr_storage))
        return -EINVAL;
    if (ulen == 0)
        return 0;
    if (copy_from_user(kaddr, uaddr, ulen))
        return -EFAULT;
    return audit_sockaddr(ulen, kaddr);
}

2 unix socket 常见使用场景

在工作中,unix socket 常见的使用场景有 3 个:进程间通信,进程间传递打开的文件描述符(fd),进程单例启动检查。本节分别举例来说明 3 种使用场景。

进程间通信

3 种类型的 unix socket 均支持该功能

进程间传递 fd

3 种类型的 unix socket 均支持该功能

进程单例启动

使用 abstract unix socket 实现

2.1 进程间通信

如下代码用 socket pair 来实现,socket pair 可以在父子进程间通信,fs unix socket 和 abstract unix socket 可以在非父子进程间通信。

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

int main() {
  int fd[2] = {0, 0};
  int sock = socketpair(AF_LOCAL, SOCK_STREAM, 0, fd);
  if (sock < 0) {
    perror("socketpair");
    exit(1);
  }

  pid_t pid = fork();
  if (pid > 0) {
    close(fd[1]);
    char buf[1024];
    while (1) {
      memset(buf, '\0', sizeof(buf));
      strcpy(buf, "hello child");
      write(fd[0], buf, sizeof(buf) - 1);

      read(fd[0], buf, sizeof(buf) - 1);
      printf("msg from child process: %s\n", buf);

      sleep(1);
    }
    close(fd[0]);
  } else if (pid == 0) {
    close(fd[0]);
    char buf[1024];
    while (1) {
      read(fd[1], buf, sizeof(buf) - 1);
      printf("msg from parent process: %s\n", buf);

      memset(buf, '\0', sizeof(buf));
      strcpy(buf, "hello parent");
      write(fd[1], buf, sizeof(buf) - 1);

      sleep(1);
    }
    close(fd[1]);
  } else {
    perror("fork");
    exit(2);
  }

  return 0;
}

2.2 进程间传递打开的文件描述符

打开的文件属于进程级的资源,一个进程可以重复打开同一个文件,每次的 fd 是不同的,不同的进程也可以打开相同的文件,fd 可能相同,也可能不同。

打开的文件描述符 fd 是一个 int 类型的数据,在进程间传递 fd,不是简单地传递这个 int 数据,而是接收方接收到这个 fd 之后,直接可以对这个文件进行访问,如果只是在进程间传递一个 int 数据,那么接收方无法访问打开的文件。

server 端,发送 fd:

#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>

#define SOCKET_PATH "./unix_socket_fd_transmit"

int create_unix_server_and_accept_connection() {
  int server_fd;
  int connection_fd;
  int result;
  struct sockaddr_un sun;

  server_fd = socket(PF_UNIX, SOCK_STREAM, 0);
  printf("server fd: %d\n", server_fd);

  unlink(SOCKET_PATH);
  memset(&sun, 0, sizeof(sun));
  sun.sun_family = AF_UNIX;
  strcpy(sun.sun_path, SOCKET_PATH);
  int len = offsetof(struct sockaddr_un, sun_path) + strlen(sun.sun_path) + 1;
  if (bind(server_fd, (struct sockaddr *)&sun, len) != 0) {
    printf("bind error\n");
    return -1;
  }

  listen(server_fd, 8);



  struct sockaddr_un client_un;
  int un_len = sizeof(client_un);
  connection_fd = accept(server_fd, (struct sockaddr *)&client_un, &un_len);

  return connection_fd;
}

void send_fd(int socket_fd, int fd_to_be_send) {
  struct msghdr msg;
  struct cmsghdr *cmsg = NULL;
  int io[3] = {0, 1, 2};
  char buf[CMSG_SPACE(sizeof(io))];
  struct iovec iov;
  int dummy;

  memset(&msg, 0, sizeof(struct msghdr));

  iov.iov_base = &dummy;
  iov.iov_len = 1;

  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
  msg.msg_control = buf;
  msg.msg_controllen = sizeof(buf);

  cmsg = CMSG_FIRSTHDR(&msg);
  cmsg->cmsg_len = CMSG_LEN(sizeof(fd_to_be_send));
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;

  memcpy(CMSG_DATA(cmsg), &fd_to_be_send, sizeof(fd_to_be_send));

  msg.msg_controllen = cmsg->cmsg_len;

  int size = sendmsg(socket_fd, &msg, 0);
  return;
}

int main(int argc, char const *argv[]) {
  int connection_fd = create_unix_server_and_accept_connection();

  int fd_to_be_send = open("./test", O_RDWR | O_CREAT);
  char buf[] = "test transmit fd by unix socket between process";
  write(fd_to_be_send, buf, sizeof(buf) + 1);

  printf("connection fd: %d, fd to be send: %d\n", connection_fd, fd_to_be_send);
  send_fd(connection_fd, fd_to_be_send);

  sleep(100);
  return 0;
}


client 端,接收 fd:

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define SOCKET_PATH "./unix_socket_fd_transmit"

int create_unix_connection() {
  int fd;
  int result;
  struct sockaddr_un sun;

  fd = socket(PF_UNIX, SOCK_STREAM, 0);

  sun.sun_family = AF_UNIX;
  strncpy(sun.sun_path, SOCKET_PATH, sizeof(sun.sun_path) - 1);

  connect(fd, (struct sockaddr *)&sun, sizeof(sun));

  return fd;
}

void receive_fd(int socket_fd) {
  int dummy = 0;
  int received_fd;

  struct iovec iov;
  iov.iov_base = &dummy;
  iov.iov_len = 1;

  char buf[CMSG_SPACE(sizeof(received_fd))];

  struct msghdr msg;
  memset(&msg, 0, sizeof(msg));
  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
  msg.msg_control = buf;
  msg.msg_controllen = sizeof(buf);

  struct cmsghdr *cmsg;
  cmsg = CMSG_FIRSTHDR(&msg);
  cmsg->cmsg_len = CMSG_LEN(sizeof(received_fd));
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;

  memcpy(CMSG_DATA(cmsg), &received_fd, sizeof(received_fd));

  int size = recvmsg(socket_fd, &msg, 0);
  memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(received_fd));
  printf("received fd: %d\n", received_fd);

  lseek(received_fd, 0, SEEK_SET);
  char receive_buf[128] = {'\0'};
  int read_rc = read(received_fd, receive_buf, 128);
  printf("content in received fd: %s, read_rc: %d\n", receive_buf, read_rc);
}

int main(int argc, char const *argv[]) {
  int socket_fd;
  int fds[3];

  socket_fd = create_unix_connection();
  receive_fd(socket_fd);

  return 0;
}

 2.3 进程单例启动

进程单例启动可以保证在一台机器上,一个进程只启动一个实例,如果进程已经启动,那么后边再次启动这个进程,就会启动失败。abstract unix socket 不和磁盘文件系统绑定,并且在进程退出时就会销毁,所以适用于进程单例启动的场景。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <errno.h>

int32_t app_can_start(char *app_path_id) {
  printf("app path id: %s\n", app_path_id);

  int32_t fd = socket(AF_UNIX, SOCK_STREAM, 0);
  if (fd < 0) {
    printf("create unix socket error: %s\n", strerror(errno));
    return 0;
  }
  printf("fd: %d\n", fd);

  struct sockaddr_un sock_addr;
  sock_addr.sun_family = AF_UNIX;
  sock_addr.sun_path[0] = '\0';
  memcpy(sock_addr.sun_path + 1, app_path_id, sizeof(sock_addr.sun_path) - 2);

  if (bind(fd, (struct sockaddr *)&sock_addr, sizeof(sock_addr)) < 0) {
    printf("unix socket bind error: %s\n", strerror(errno));
    close(fd);
    return 0;
  }

  if (listen(fd, 65536) < 0) {
    printf("unix socket listen error: %s\n", strerror(errno));
    close(fd);
    return 0;
  }

  return 1;
}

int main() {
  if (app_can_start("test")) {
    printf("first start\n");
  } else {
    printf("already started\n");
  }
  sleep(100);
  return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值