C 语言的协程探秘

目录

一、概念

二、核心函数

getcontext(ucontext_t *ucp):

setcontext(const ucontext_t *ucp):

makecontext(ucontext_t *ucp, void (*func)(), int argc, ...):

swapcontext(ucontext_t *oucp, const ucontext_t *ucp):

三、ucontext_t 结构体

uc_link:

uc_stack:

uc_mcontext:

uc_sigmask:

四、代码实践

关键点

 五、hook实践

1. Hook 的基本概念

2. Hook 的实现方式

3.代码实践 

定义函数指针类型和变量

Hook 函数实现

另一个 Hook 函数

初始化 Hook

主函数

总结

六、总结

协程的基本概念

协程在网络 I/O 处理中的应用

伪代码框架解析


一、概念

在 C 语言中,ucontext 库提供了一套功能丰富的 API,用于管理程序的执行上下文。这些功能主要用于实现用户空间的轻量级线程(协程),允许程序在多个执行流之间进行显式的上下文切换。

  • 上下文(Context):表示程序在某一时间点的运行状态,包括程序计数器、寄存器集合和堆栈等。
  • 协程(Coroutine):是一种比线程更轻量的执行单元,它允许进行非抢占式的任务切换,即在用户空间进行切换而无需内核介入。

二、核心函数

  1. getcontext(ucontext_t *ucp):
    • 获取当前执行流的上下文,并保存在 ucp 指向的 ucontext_t 结构体中。
  2. setcontext(const ucontext_t *ucp):
    • 切换到 ucp 指定的上下文执行。调用此函数后,程序的控制流会转移到新的上下文继续执行,当前函数不会返回。
  3. makecontext(ucontext_t *ucp, void (*func)(), int argc, ...):
    • 修改 ucp 指定的上下文,设置它的执行函数和函数参数。func 应是一个不返回值的函数,因为从该函数返回将导致程序终止。
  4. swapcontext(ucontext_t *oucp, const ucontext_t *ucp):
    • 保存当前上下文到 oucp 并切换到 ucp 指向的上下文。通常用于实现协程之间的切换。

三、ucontext_t 结构体

ucontext_t 结构体是管理上下文的核心,它包括以下几个部分:

  • uc_link:
    • 当前上下文结束后,程序执行将继续的上下文的指针。
  • uc_stack:
    • 描述当前上下文使用的堆栈的信息,包括堆栈的位置、大小和标志。
  • uc_mcontext:
    • 保存特定于机器的执行上下文信息(如寄存器),这部分是系统特定的。
  • uc_sigmask:
    • 上下文执行时的信号屏蔽集,用于控制哪些信号在此上下文中被阻塞。

四、代码实践

#include <stdio.h>
#include <ucontext.h>

ucontext_t ctx[3]; // 创建三个上下文对象,对应三个协程
ucontext_t main_ctx; // 主上下文,用于协程之间的调度

int count = 0; // 全局变量,用于控制打印次数和协程切换

// coroutine1
void func1(void) {
    // 当 count 小于 30 时持续执行
	while (count ++ < 30) {
		printf("1\n"); // 打印标识符1
		swapcontext(&ctx[0], &main_ctx); // 从协程1切换到主上下文
		printf("4\n"); // 当协程1再次获得控制时,打印标识符4
	}
}

// coroutine2
void func2(void) {
    // 当 count 小于 30 时持续执行
	while (count ++ < 30) {
		printf("2\n"); // 打印标识符2
		swapcontext(&ctx[1], &main_ctx); // 从协程2切换到主上下文
		printf("5\n"); // 当协程2再次获得控制时,打印标识符5
	}
}

// coroutine3
void func3(void) {
    // 当 count 小于 30 时持续执行
	while (count ++ < 30) {
		printf("3\n"); // 打印标识符3
		swapcontext(&ctx[2], &main_ctx); // 从协程3切换到主上下文
		printf("6\n"); // 当协程3再次获得控制时,打印标识符6
	}
}

// schedule
int main() {
    // 分配堆栈空间
	char stack1[2048] = {0};
	char stack2[2048] = {0};
	char stack3[2048] = {0};

    // 初始化协程1的上下文,并设置堆栈和链接
	getcontext(&ctx[0]);
	ctx[0].uc_stack.ss_sp = stack1;
	ctx[0].uc_stack.ss_size = sizeof(stack1);
	ctx[0].uc_link = &main_ctx;
	makecontext(&ctx[0], func1, 0);

    // 初始化协程2的上下文,并设置堆栈和链接
	getcontext(&ctx[1]);
	ctx[1].uc_stack.ss_sp = stack2;
	ctx[1].uc_stack.ss_size = sizeof(stack2);
	ctx[1].uc_link = &main_ctx;
	makecontext(&ctx[1], func2, 0);

    // 初始化协程3的上下文,并设置堆栈和链接
	getcontext(&ctx[2]);
	ctx[2].uc_stack.ss_sp = stack3;
	ctx[2].uc_stack.ss_size = sizeof(stack3);
	ctx[2].uc_link = &main_ctx;
	makecontext(&ctx[2], func3, 0);

    // 开始从主上下文切换到协程
	printf("swapcontext\n");
	while (count <= 30) { // 调度器,循环切换到各个协程
		swapcontext(&main_ctx, &ctx[count % 3]); // 循环切换到三个协程
	}

	printf("\n");
}
关键点
  1. 初始化上下文:为每个协程分配堆栈并设置其执行的函数。
  2. 协程调度:主函数中的 while 循环实现简单的轮转调度(Round-Robin),根据 count 的值循环切换到三个协程的上下文。
  3. swapcontext():保存当前上下文并激活新的上下文。在每个协程中,它将控制权交回给主上下文,而主上下文则根据调度逻辑选择下一个要运行的协程。

这段代码通过 swapcontext() 在协程和主控制流之间来回切换,展示了基本的协程调度。

 五、hook实践

1. Hook 的基本概念

Hook 是一种编程技术,用于拦截和修改函数或方法的调用。通过 hook,程序员可以在函数调用前后插入自定义逻辑,从而改变程序的行为。Hook 常用于以下几个场景:

  • 调试:插入调试代码来监控函数调用及其参数和返回值。
  • 监控:监控系统或应用程序的行为,例如记录日志。
  • 扩展功能:在不修改原始代码的情况下添加新的功能。
  • 安全性:拦截和修改系统调用以增强安全性。
2. Hook 的实现方式

Hook 的实现方式有多种,常见的包括:

  • 动态链接库插桩:使用动态链接库 (DLL) 的加载机制来替换或扩展现有函数。
  • 代码注入:将自定义代码注入到目标进程中。
  • 函数指针替换:修改函数指针,使其指向自定义的函数。
  • 虚函数表 (VTable) 替换:在 C++ 中,通过替换对象的虚函数表条目来实现 hook。
3.代码实践 

 动态链接库插桩和函数指针替换的方式来实现 hook。代码的主要目标是拦截 readwrite 函数的调用。



#define _GNU_SOURCE

#include <dlfcn.h>

#include <stdio.h>
#include <ucontext.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>

#include <pthread.h>
#include <sys/poll.h>
#include <sys/epoll.h>

#if 1
// hook
typedef ssize_t (*read_t)(int fd, void *buf, size_t count);
read_t read_f = NULL;

typedef ssize_t (*write_t)(int fd, const void *buf, size_t count);
write_t write_f = NULL;

ssize_t read(int fd, void *buf, size_t count)
{

	struct pollfd fds[1] = {0};

	fds[0].fd = fd;
	fds[0].events = POLLIN;

	int res = poll(fds, 1, 0);
	if (res <= 0)
	{ //

		// fd --> epoll_ctl();

		swapcontext(); // fd --> ctx
	}
	// io

	ssize_t ret = read_f(fd, buf, count);
	printf("read: %s\n", (char *)buf);
	return ret;
}

ssize_t write(int fd, const void *buf, size_t count)
{

	printf("write: %s\n", (const char *)buf);

	return write_f(fd, buf, count);
}

void init_hook(void)
{

	if (!read_f)
	{
		read_f = dlsym(RTLD_NEXT, "read");
	}

	if (!write_f)
	{
		write_f = dlsym(RTLD_NEXT, "write");
	}
}

#endif

int main()
{

	init_hook();

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(struct sockaddr_in));

	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(2048);

	if (-1 == bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr)))
	{
		perror("bind");
		return -1;
	}

	listen(sockfd, 10);

	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(clientaddr);
	int clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len);
	printf("accept\n");

	while (1)
	{

		char buffer[128] = {0};
		int count = read(clientfd, buffer, 128);
		if (count == 0)
		{
			break;
		}
		write(clientfd, buffer, count);
		printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, clientfd, count, buffer);
	}

	return 0;
}
定义函数指针类型和变量
typedef ssize_t (*read_t)(int fd, void *buf, size_t count);
read_t read_f = NULL;

typedef ssize_t (*write_t)(int fd, const void *buf, size_t count);
write_t write_f = NULL;

定义了两个函数指针类型 read_twrite_t,并声明了两个全局变量 read_fwrite_f。这些变量将用于保存原始的 readwrite 函数地址。

Hook 函数实现
ssize_t read(int fd, void *buf, size_t count) {
    struct pollfd fds[1] = {0};

    fds[0].fd = fd;
    fds[0].events = POLLIN;

    int res = poll(fds, 1, 0);
    if (res <= 0) {
        getcontext(&main_ctx);
        swapcontext(&main_ctx, &read_ctx); // fd --> ctx
    }

    ssize_t ret = read_f(fd, buf, count);
    printf("read: %s\n", (char *)buf);
    return ret;
}

这是自定义的 read 函数,它会拦截所有对 read 函数的调用,并在调用原始 read 函数之前执行一些逻辑。

  1. poll 调用:使用 poll 检查文件描述符 fd 是否有数据可读。
  2. 上下文切换:如果没有数据可读,保存当前上下文并切换到 read_ctx
  3. 调用原始 read:调用保存的原始 read 函数 read_f
  4. 打印读取的数据:将读取的数据打印出来。
另一个 Hook 函数
ssize_t write(int fd, const void *buf, size_t count) {
    printf("write: %s\n", (const char *)buf);
    return write_f(fd, buf, count);
}

 自定义的 write 函数会拦截所有对 write 函数的调用,打印写入的数据,并调用原始 write 函数。

初始化 Hook
void init_hook(void) {
    if (!read_f) {
        read_f = dlsym(RTLD_NEXT, "read");
    }
    if (!write_f) {
        write_f = dlsym(RTLD_NEXT, "write");
    }
}

init_hook 函数用于初始化 read_fwrite_f,它们分别指向原始的 readwrite 函数。dlsym 函数从共享库中查找符号的地址,这里查找的是 readwrite 函数。

主函数
int main() {
    init_hook();

    // 初始化 read_ctx 上下文
    getcontext(&read_ctx);
    read_ctx.uc_link = &main_ctx;
    read_ctx.uc_stack.ss_sp = malloc(SIGSTKSZ);
    read_ctx.uc_stack.ss_size = SIGSTKSZ;
    makecontext(&read_ctx, (void (*)(void))read, 0);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2048);

    if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {
        perror("bind");
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf("accept\n");

    while (1) {
        char buffer[128] = {0};
        int count = read(clientfd, buffer, 128);
        if (count == 0) {
            break;
        }
        write(clientfd, buffer, count);
        printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, clientfd, count, buffer);
    }

    free(read_ctx.uc_stack.ss_sp);

    return 0;
}

在主函数中,首先调用 init_hook 初始化 read_fwrite_f。然后初始化 read_ctx 上下文,并使用 makecontext 函数设置上下文的执行函数为 read

接下来,创建一个套接字,并绑定到本地地址,开始监听连接。接受一个客户端连接后,进入循环,读取客户端发送的数据,并将数据回写给客户端。

通过这种方式,readwrite 的行为被拦截,并增加了自定义逻辑。

总结

通过 hook 技术,可以拦截系统函数的调用,并在调用之前或之后执行自定义逻辑。这在调试、监视和修改函数行为时非常有用。在代码中,hook 技术用于拦截 readwrite 函数的调用,打印读取和写入的数据,并实现上下文切换的逻辑。

六、总结

协程的基本概念

协程(Coroutines) 是一种程序组件,允许不同的执行线程或路径在单个进程中共享同一个执行环境。与线程相比,协程是轻量级的,因为它们共享相同的进程空间并具有较小的上下文切换开销。主要特点包括:

  • 非阻塞执行:协程可以在等待 I/O 操作或其他长时间执行的任务时让出 CPU,允许其他协程运行。
  • 协作式多任务处理:协程的切换是显式的,由程序员或协程库控制,不是由操作系统的调度器控制。
  • 高效的并发:协程可以在单个线程中实现高效的并发执行,适用于处理大量并发连接和请求。
协程在网络 I/O 处理中的应用

在网络应用中,协程可用于优化 I/O 操作,尤其是在高并发环境下。使用协程可以避免为每个客户端连接创建一个线程的高成本,从而减少资源消耗和提高性能。

伪代码框架解析

代码定义了一个协程框架的结构,从协程的定义开始逐步解析:

struct coroutine{
    int fd;                 // 文件描述符,用于网络 I/O
    ucontext_t ctx;        // 上下文信息,用于协程切换
    void *arg;             // 函数参数
    queue_node(coroutine, ) ready_queue;  // 准备就绪的协程队列
    rbtree_node(coroutine, ) wait_rb;     // 等待中的协程红黑树
    rbtree_node(coroutine, ) sleep_rb;    // 睡眠中的协程红黑树
};

这里,每个协程包括一个文件描述符、上下文信息、函数参数以及三种状态的管理节点:就绪、等待和睡眠。

void func(void){
}
typedef void *(*coroutine_entry)(void *)
int create_coroutine(co_id, coroutine_entry entry, void *arg){
    struct coroutine *co = malloc(sizeof(struct coroutine));
    co->ss_sp = 
    makecontext(&co->ctx, func, 0);
}

在这段代码中,create_coroutine 函数用于初始化一个协程。它分配内存,设置栈空间,并使用 makecontext 准备上下文切换。func 是协程将要执行的函数。

// 调度器
struct scheduler{
    int epfd;                          // epoll 文件描述符
    struct epoll_event events[];       // 事件数组

    queue_node(coroutine, ) ready_head;  // 就绪队列头
    rbtree_root(coroutine, ) wait;        // 等待红黑树
    rbtree_root(coroutine, ) sleep;       // 睡眠红黑树
}

调度器负责管理所有协程的生命周期和状态转换。它使用 epoll 监听事件,根据 I/O 事件调度协程。就绪、等待和睡眠队列/树用于管理不同状态的协程。

通过使用协程,你可以高效地管理数以万计的并发连接,每个连接使用非常少的内存,同时避免多线程编程中常见的同步和竞争状态问题。

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值