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;
}