一、虚拟文件系统
在一切皆文件的理念中,套接字也属于文件系统。我们需要支持新的文件系统,因此需要虚拟文件系统统一管理。虚拟文件系统即是把文件系统中重要的系统调用抽象出来,对每种文件系统单独做处理:
typedef struct fs_op_t
{
int (*mkfs)(dev_t dev, int args);
int (*super)(dev_t dev, super_t *super);
int (*open)(inode_t *dir, char *name, int flags, int mode, inode_t **result);
void (*close)(inode_t *inode);
int (*read)(inode_t *inode, char *data, int len, off_t offset);
int (*write)(inode_t *inode, char *data, int len, off_t offset);
int (*truncate)(inode_t *inode);
int (*stat)(inode_t *inode, stat_t *stat);
int (*permission)(inode_t *inode, int mask);
int (*namei)(inode_t *dir, char *name, char **next, inode_t **result);
int (*mkdir)(inode_t *dir, char *name, int mode);
int (*rmdir)(inode_t *dir, char *name);
int (*link)(inode_t *odir, char *oldname, inode_t *ndir, char *newname);
int (*unlink)(inode_t *dir, char *name);
int (*mknod)(inode_t *dir, char *name, int mode, int dev);
int (*readdir)(inode_t *inode, dentry_t *entry, size_t count, off_t offset);
} fs_op_t;
fs_op_t *fs_ops[FS_TYPE_NUM];
当前我们已有minix和管道两个文件系统,这里不做具体的展示,例如minix定义函这个函数后,封装为fs_op_t结构,并注册到fs_ops中。即可通过fs_op_t对minix文件系统进行一系列的操作。
二、套接字
伯克利套接字(Berkeley sockets) 最初随着 4.2BSD(Berkeley Software Distribution) UNIX 操作系统一同发布,作为一种编程接口。目前大多数其他编程语言都提供类似的接口,通常以基于 C API 的包装器库的形式编写。
2.1、套接字协议组定义
套接字作为对用户态提供的网络编程接口,定义了许多协议组,因为它不仅要支持TCP协议,也要支持UDP协议,以及更多其他协议。
enum
{
AF_UNSPEC = 0, // 未定义
AF_PACKET, // 数据包
AF_INET, // IPV4
};
enum
{
SOCK_STREAM = 1, // 数据流
SOCK_DGRAM = 2, // 数据报
SOCK_RAW = 3, // 原始套接字
};
enum
{
PROTO_IP = 0,
PROTO_ICMP = 1,
PROTO_TCP = 6,
PROTO_UDP = 17,
};
typedef enum socktype_t // 套接字的类型
{
SOCK_TYPE_NONE = 0,
SOCK_TYPE_PKT, // 数据包
SOCK_TYPE_RAW, // 原始套接字
SOCK_TYPE_TCP,
SOCK_TYPE_UDP,
SOCK_TYPE_NUM,
} socktype_t;
typedef struct sockaddr_t // 套接字地址
{
u16 family;
char data[14];
} sockaddr_t;
typedef struct sockaddr_in_t
{
u16 family;
u16 port;
ip_addr_t addr;
u8 zero[8];
} sockaddr_in_t;
typedef struct iovec_t // 输入输出向量,实质是缓冲区
{
size_t size;
void *base;
} iovec_t;
typedef struct msghdr_t
{
sockaddr_t *name;
int namelen;
iovec_t *iov;
int iovlen;
} msghdr_t;
typedef struct socket_t // socket描述符
{
socktype_t type; // socket 类型
} socket_t;
typedef struct socket_op_t // 封装为虚拟操作系统
{
int (*socket)(socket_t *s, int domain, int type, int protocol);
int (*close)(socket_t *s);
int (*listen)(socket_t *s, int backlog);
int (*accept)(socket_t *s, sockaddr_t *addr, int *addrlen, socket_t **ns);
int (*bind)(socket_t *s, const sockaddr_t *name, int namelen);
int (*connect)(socket_t *s, const sockaddr_t *name, int namelen);
int (*shutdown)(socket_t *s, int how);
int (*getpeername)(socket_t *s, sockaddr_t *name, int *namelen);
int (*getsockname)(socket_t *s, sockaddr_t *name, int *namelen);
int (*getsockopt)(socket_t *s, int level, int optname, void *optval, int *optlen);
int (*setsockopt)(socket_t *s, int level, int optname, const void *optval, int optlen);
int (*recvmsg)(socket_t *s, msghdr_t *msg, u32 flags);
int (*sendmsg)(socket_t *s, msghdr_t *msg, u32 flags);
} socket_op_t;
2.2、套接字的实现
套接字也作为一个虚拟操作系统进行实现,需要定义open,close等函数。
#define BACKLOG 5
static socket_op_t *socket_ops[SOCK_TYPE_NUM];
static inode_t *socket_create()
{
inode_t *inode = get_free_inode();
// 区别于 EOF 这里是无效的设备,但是被占用了
inode->dev = -FS_TYPE_SOCKET;
inode->desc = NULL;
inode->count = 1;
inode->type = FS_TYPE_SOCKET;
inode->op = fs_get_op(FS_TYPE_SOCKET);
return inode;
}
static socket_t *socket_get(fd_t fd)
{
task_t *task = running_task();
file_t *file = task->files[fd];
if (!file)
return NULL;
inode_t *inode = file->inode;
if (!inode)
return NULL;
if (!inode->desc)
return NULL;
return (socket_t *)inode->desc;
}
int sys_socket(int domain, int type, int protocol)
{
socktype_t socktype = SOCK_TYPE_NONE;
file_t *file;
fd_t fd = fd_get(&file);
if (fd < EOK)
return fd;
inode_t *inode = socket_open();
file->inode = inode;
file->flags = 0;
file->count = 1;
file->offset = 0;
socket_t *s = (socket_t *)inode->desc;
s->type = socktype;
if (s->type != SOCK_TYPE_NONE)
{
socket_get_op(s->type)->socket(s, domain, type, protocol);
}
return fd;
}
int sys_recvmsg(int fd, msghdr_t *msg, u32 flags)
{
socket_t *s = socket_get(fd);
if (!s)
return -EINVAL;
msghdr_t m;
memcpy(&m, msg, sizeof(msghdr_t));
m.iov = iovec_dup(msg->iov, msg->iovlen);
int ret = iovec_check(m.iov, m.iovlen, true);
if (ret < EOK)
return ret;
ret = socket_get_op(s->type)->recvmsg(s, &m, flags);
kfree(m.iov);
return ret;
}
void socket_init()
{
fs_register_op(FS_TYPE_SOCKET, &socket_op);
}
如上所示,在创建一个socket时,也需要申请一个inode进行管理,打开和关闭socket就是打开inode和释放inode。在我们监控链接、监听,收发消息本质是一系列的系统调用。我们展示的代码中只保留了 sys_recvmsg 这一个系统调用,本质还是调用虚拟文件系统的 recvmsg 函数。
三、数据包套接字
数据包套接字 用于在设备驱动程序 (OSI第2层) 级别接收或发送原始数据包。它们允许用户在物理层之上的用户空间中实现协议模块。它的主要用途就是用来抓包。通过它可以直接发送以太网数据包:
typedef struct pkt_pcb_t
{
list_node_t node; // 链表节点,用于连接到全局PCB列表
list_t rx_pbuf_list; // 缓冲地址
eth_addr_t laddr; // 本地mac地址
eth_addr_t raddr; // 远端mac地址
struct task_t *rx_waiter; // 等待接收的进程
} pkt_pcb_t;
static list_t pkt_pcb_list;
static int pkt_socket(socket_t *s, int domain, int type, int protocol) // 创建套接字
{
if (!s->pkt)
{
s->pkt = (pkt_pcb_t *)kmalloc(sizeof(pkt_pcb_t));
memset(s->pkt, 0, sizeof(pkt_pcb_t));
}
pkt_pcb_t *pcb = s->pkt;
list_init(&pcb->rx_pbuf_list);
list_push(&pkt_pcb_list, &pcb->node);
return EOK;
}
static int pkt_bind(socket_t *s, const sockaddr_t *name, int namelen) // 绑定本地MAC地址
{
sockaddr_ll_t *sin = (sockaddr_ll_t *)name;
eth_addr_copy(s->pkt->laddr, sin->addr);
return EOK;
}
static int pkt_connect(socket_t *s, const sockaddr_t *name, int namelen) // 连接远端MAC地址
{
sockaddr_ll_t *sin = (sockaddr_ll_t *)name;
eth_addr_copy(s->pkt->raddr, sin->addr);
return EOK;
}
static int pkt_getpeername(socket_t *s, sockaddr_t *name, int *namelen)
{
sockaddr_ll_t *sin = (sockaddr_ll_t *)name;
sin->family = AF_PACKET;
ip_addr_copy(sin->addr, s->pkt->raddr);
*namelen = sizeof(sockaddr_ll_t);
return EOK;
}
static int pkt_recvmsg(socket_t *s, msghdr_t *msg, u32 flags)
{
err_t ret = EOK;
if (list_empty(&s->pkt->rx_pbuf_list))
{
s->pkt->rx_waiter = running_task();
ret = task_block(s->pkt->rx_waiter, NULL, TASK_WAITING, TIMELESS);
}
if (ret != EOK)
return ret;
pbuf_t *pbuf = element_entry(pbuf_t, node, list_popback(&s->pkt->rx_pbuf_list));
ret = iovec_write(msg->iov, msg->iovlen, pbuf->payload, pbuf->length);
pbuf_put(pbuf);
return ret;
}
static int pkt_sendmsg(socket_t *s, msghdr_t *msg, u32 flags)
{
int ret = EOK;
size_t size = iovec_size(msg->iov, msg->iovlen);
if (size > ETH_MTU)
return -EINVAL;
if (msg->name)
ret = pkt_connect(s, msg->name, msg->namelen);
if (ret < EOK)
return EOK;
pbuf_t *pbuf = pbuf_get();
ret = iovec_read(msg->iov, msg->iovlen, pbuf->payload, size);
if (ret < EOK)
return ret;
pbuf->length = size;
netif_t *netif = netif_get();
netif_output(netif, pbuf);
return size;
}
static int pkt_recv(pkt_pcb_t *pcb, pbuf_t *pbuf)
{
eth_t *eth = pbuf->eth;
if (!eth_addr_isany(pcb->raddr) && !eth_addr_cmp(pcb->raddr, eth->src))
return false;
if (!eth_addr_isany(pcb->laddr) && !eth_addr_cmp(pcb->laddr, eth->dst))
return false;
pbuf->count++;
list_push(&pcb->rx_pbuf_list, &pbuf->node);
if (pcb->rx_waiter)
{
task_unblock(pcb->rx_waiter, EOK);
pcb->rx_waiter = NULL;
}
return true;
}
err_t pkt_input(netif_t *netif, pbuf_t *pbuf)
{
int eaten = false;
list_t *list = &pkt_pcb_list;
for (list_node_t *ptr = list->head.next; ptr != &list->tail; ptr = ptr->next)
{
pkt_pcb_t *pcb = element_entry(pkt_pcb_t, node, ptr);
err_t ret = pkt_recv(pcb, pbuf);
if (ret < 0)
return ret;
if (ret > 0)
{
eaten = true;
break;
}
}
return eaten;
}
这是一个内核级套接字实现,提供了对数据链路层的直接访问能力。数据收发直接操作以太网帧。例如pkt_sendmsg 内部直接调用的 netif_output,这是以太网的发送。它不涉及ARP、IP等协议,为应用程序提供对网络硬件的底层访问能力。
四、原始套接字
原始套接字允许在用户空间中实现新的 IPv4 协议。原始套接字 接收或发送 不包括 链路层报头的原始数据报。即原始套接字发送IP数据包,它的实现和数据包套接字比较类似。例如原始套接字在 raw_sendmsg中 调用的是 ip_output 函数。我们这里不再详细贴原始套接字的实现,只看一下如何使用测试的。
#define BUFLEN 0x1000
static char buf[BUFLEN];
int main(int argc, char const *argv[])
{
int fd = socket(AF_INET, SOCK_RAW, PROTO_IP); // 创建一个原始套接字,这里的socket是系统调用
msghdr_t msg;
iovec_t iov;
msg.name = NULL;
msg.namelen = 0;
msg.iov = &iov;
msg.iovlen = 1;
iov.base = buf;
iov.size = sizeof(buf);
printf("receiving...\n");
int ret = recvmsg(fd, &msg, 0); // 等到接收数据包
printf("recvmsg %d\n", ret);
ip_t *ip = (ip_t *)iov.base;
printf("recv ip %r -> %r : %s\n", ip->src, ip->dst, ip->payload);
ip_addr_t addr;
ip_addr_copy(addr, ip->dst);
ip_addr_copy(ip->dst, ip->src);
ip_addr_copy(ip->src, addr);
memcpy(ip->payload, "this is ack message", 19);
iov.size = sizeof(ip_t) + 19;
sendmsg(fd, &msg, 0); // 发送一个数据包
close(fd);
return EOK;
}
我们这里拿recvmsg举例,这是一个系统调用。在sys_recvmsg中会调用虚拟文件系统的 sendmsg 方法,因为这里是原始套接字,因此会去调用raw_recvmsg。在raw_recvmsg判断接收队列是否有数据包,如果没有就阻塞进程等待,否则就从接收队列中获取数据包,并将数据写入缓冲区。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付