[{"content":"引言 ​\t有关project2的实验，对应课程网址如下6.1810 / Fall 2024\nsystem call tracing(moderate) 前置知识 本实验使用位图mask记录需要追踪的系统调用，如果对应位置为1，就会监听对应的系统调用\nxv6下系统调用的流程（后续的每一个操作，几乎都可以在源码层面进行体现，需细心品读.c或者.S文件）\n用户态下调用系统调用函数，系统调用参数会被放到a0 - a6的寄存器当中\n执行user/usys.S中对应系统调用函数的汇编指令\n将系统调用号放在寄存器a7当中 调用ecall指令，ecall会进行的硬件操作 保存中断使能状态， 关中断， 切换到内核态， 跳转执行执行kernel/trampoline.S中的uservec中（陷阱处理入口）的指令 kernel/trampoline.S中uservec的执行\n先保存a0寄存器 (csrw sscratch, a0) ，让a0寄存器可以空出来保存TRAPFRAME\na0放TRAPFRAME (li a0, TRAPFRAME)，TRAPFRAME是proc中的一个结构体，保存用户寄存器状态\n暂存用户态寄存器 (sd ra, 40(a0) \u0026hellip;\u0026hellip;)\n将原来的a0也保存到TRAPFRAME当中\ncsrr t0, sscratch sd t0, 112(a0)\n后续的一些过程\n1 2 3 4 5 6 7 8 9 10 11 # initialize kernel stack pointer, from p-\u0026gt;trapframe-\u0026gt;kernel_sp ld sp, 8(a0) # make tp hold the current hartid, from p-\u0026gt;trapframe-\u0026gt;kernel_hartid ld tp, 32(a0) # load the address of usertrap(), from p-\u0026gt;trapframe-\u0026gt;kernel_trap ld t0, 16(a0) # fetch the kernel page table address, from p-\u0026gt;trapframe-\u0026gt;kernel_satp. ld t1, 0(a0) 再往后\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # wait for any previous memory operations to complete, so that # they use the user page table. # 主要是刷新TLB sfence.vma zero, zero # 确保 CPU 已完成所有未完成的用户态访存操作，防止切换页表后丢失写入。 # install the kernel page table. csrw satp, t1 # flush now-stale user entries from the TLB. sfence.vma zero, zero # 清除 TLB 中缓存的用户页表项，避免后续内核代码使用错误的地址翻译。 # jump to usertrap(), which does not return jr t0 执行kernel/trap.c中的usertrap()函数\n检查是否之前是用户态，陷阱向量更改为内核陷阱向量，(当前内核态，下次陷阱走内核陷阱向量) 保存用户的pc程序计数器 陷入原因是系统调用，将原来用户态pc值 + “1” 开中断 调用kernel/syscall.c中的syscall() kernel/syscall.c中syscall()的调用过程\n获取系统调用号执行对应操作，将结果放在a0寄存器当中 p-\u0026gt;trapframe-\u0026gt;a0 = syscalls[num]() kernel/syscall.c中syscalls[]数组成员执行，从a0等寄存器中获取系统调用的参数，参数传递到对应的操作当中\n执行完成后调用usertrapret函数\n关中断 陷阱向量改为用户的陷阱向量trampoline_uservec 初始化陷阱帧，保存内核相关信息 配置返回状态 挑战到trampoline.S中的userret tramploline.S中的userret的执行\n切换用户页表\n# switch to the user page table. sfence.vma zero, zero csrw satp, a0 sfence.vma zero, zero 恢复寄存器\n1 2 3 4 5 6 7 8 li a0, TRAPFRAME # restore all but a0 from TRAPFRAME ld ra, 40(a0) ...... # restore user a0 ld a0, 112(a0) 返回用户态和复原用户pc\n具体实现 了解了系统调用的过程，我们要做的就是根据trace_mask来追踪每一次的系统调用过程\n首先需要在proc中定义trace_mask，用于之后的使用\n调用trace系统调用的时候，将参数传递给trace_mask，这一步在kernel/sysproc.c中实现sys_trace()来完成\n上面两步实现了通过trace系统调用在proc中添加trace_mask\n下面还要实现通过系统调用号打印对应的系统调用\nkernel/syscall.c中syscall可以获得系统调用号，与trace_mask判断本次系统调用是否被监听了，如果监听了，就进行打印。\n注意对于fork的调用，也需要父进程的p-\u0026gt;trace_mask传给子进程的p-\u0026gt;trace_mask，否则子进程监听就不对了\n最后实现trace程序\ntrace程序接受trace_mask和监听命令作为参数 先调用trace系统调用初始化trace_mask 再通过exec执行后面的命令及看监听指定系统调用 kernel/proc.h中struct proc的修改\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // Per-process state struct proc { struct spinlock lock; int trace_mask; // 用于记录程序需要追踪的系统调用 // p-\u0026gt;lock must be held when using these: enum procstate state; // Process state void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parent\u0026#39;s wait int pid; // Process ID // wait_lock must be held when using this: struct proc *parent; // Parent process // these are private to the process, so p-\u0026gt;lock need not be held. uint64 kstack; // Virtual address of kernel stack uint64 sz; // Size of process memory (bytes) pagetable_t pagetable; // User page table struct trapframe *trapframe; // data page for trampoline.S struct context context; // swtch() here to run process struct file *ofile[NOFILE]; // Open files struct inode *cwd; // Current directory char name[16]; // Process name (debugging) }; kernel/sysproc.c中的sys_trace()函数 1 2 3 4 5 6 7 8 9 10 uint64 sys_trace(void) { int mask; argint(0, \u0026amp;mask); // 系统调用传参 struct proc *p = myproc(); p-\u0026gt;trace_mask = mask; return 0; } kernel/proc.c中fork()的修改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 int fork(void) { int i, pid; struct proc *np; struct proc *p = myproc(); // Allocate process. if((np = allocproc()) == 0){ return -1; } // Copy user memory from parent to child. if(uvmcopy(p-\u0026gt;pagetable, np-\u0026gt;pagetable, p-\u0026gt;sz) \u0026lt; 0){ freeproc(np); release(\u0026amp;np-\u0026gt;lock); return -1; } np-\u0026gt;sz = p-\u0026gt;sz; // copy saved user registers. *(np-\u0026gt;trapframe) = *(p-\u0026gt;trapframe); // copy trace_mask 用于实现trace np-\u0026gt;trace_mask = p-\u0026gt;trace_mask; // Cause fork to return 0 in the child. np-\u0026gt;trapframe-\u0026gt;a0 = 0; // increment reference counts on open file descriptors. for(i = 0; i \u0026lt; NOFILE; i++) if(p-\u0026gt;ofile[i]) np-\u0026gt;ofile[i] = filedup(p-\u0026gt;ofile[i]); np-\u0026gt;cwd = idup(p-\u0026gt;cwd); safestrcpy(np-\u0026gt;name, p-\u0026gt;name, sizeof(p-\u0026gt;name)); pid = np-\u0026gt;pid; release(\u0026amp;np-\u0026gt;lock); acquire(\u0026amp;wait_lock); np-\u0026gt;parent = p; release(\u0026amp;wait_lock); acquire(\u0026amp;np-\u0026gt;lock); np-\u0026gt;state = RUNNABLE; release(\u0026amp;np-\u0026gt;lock); return pid; } syscall.h添加trace系统调用号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // System call numbers #define SYS_fork 1 #define SYS_exit 2 #define SYS_wait 3 #define SYS_pipe 4 #define SYS_read 5 #define SYS_kill 6 #define SYS_exec 7 #define SYS_fstat 8 #define SYS_chdir 9 #define SYS_dup 10 #define SYS_getpid 11 #define SYS_sbrk 12 #define SYS_sleep 13 #define SYS_uptime 14 #define SYS_open 15 #define SYS_write 16 #define SYS_mknod 17 #define SYS_unlink 18 #define SYS_link 19 #define SYS_mkdir 20 #define SYS_close 21 #define SYS_trace 22 kernel/syscall.c中syscall的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 char* name_index[] = {\t// 自定义的系统调用号和系统调用名的映射 [SYS_fork] \u0026#34;fork\u0026#34;, [SYS_exit] \u0026#34;exit\u0026#34;, [SYS_wait] \u0026#34;wait\u0026#34;, [SYS_pipe] \u0026#34;pipe\u0026#34;, [SYS_read] \u0026#34;read\u0026#34;, [SYS_kill] \u0026#34;kill\u0026#34;, [SYS_exec] \u0026#34;exec\u0026#34;, [SYS_fstat] \u0026#34;fstat\u0026#34;, [SYS_chdir] \u0026#34;chdir\u0026#34;, [SYS_dup] \u0026#34;dup\u0026#34;, [SYS_getpid] \u0026#34;getpid\u0026#34;, [SYS_sbrk] \u0026#34;sbrk\u0026#34;, [SYS_sleep] \u0026#34;sleep\u0026#34;, [SYS_uptime] \u0026#34;uptime\u0026#34;, [SYS_open] \u0026#34;open\u0026#34;, [SYS_write] \u0026#34;write\u0026#34;, [SYS_mknod] \u0026#34;mknod\u0026#34;, [SYS_unlink] \u0026#34;unlink\u0026#34;, [SYS_link] \u0026#34;link\u0026#34;, [SYS_mkdir] \u0026#34;mkdir\u0026#34;, [SYS_close] \u0026#34;close\u0026#34;, [SYS_trace] \u0026#34;trace\u0026#34;, }; void syscall(void) { int num; struct proc *p = myproc(); num = p-\u0026gt;trapframe-\u0026gt;a7; // num = * (int *) 0; if(num \u0026gt; 0 \u0026amp;\u0026amp; num \u0026lt; NELEM(syscalls) \u0026amp;\u0026amp; syscalls[num]) { // Use num to lookup the system call function for num, call it, // and store its return value in p-\u0026gt;trapframe-\u0026gt;a0 p-\u0026gt;trapframe-\u0026gt;a0 = syscalls[num](); } else { printf(\u0026#34;%d %s: unknown sys call %d\\n\u0026#34;, p-\u0026gt;pid, p-\u0026gt;name, num); p-\u0026gt;trapframe-\u0026gt;a0 = -1; } if (((1u \u0026lt;\u0026lt; num) \u0026amp; p-\u0026gt;trace_mask) != 0) { printf(\u0026#34;%d: syscall %s -\u0026gt; %ld\\n\u0026#34;, p-\u0026gt;pid, name_index[num], p-\u0026gt;trapframe-\u0026gt;a0); } } user/trace.c的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include \u0026#34;kernel/param.h\u0026#34; #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; int main(int argc, char *argv[]) { int i; char *nargv[MAXARG]; if(argc \u0026lt; 3 || (argv[1][0] \u0026lt; \u0026#39;0\u0026#39; || argv[1][0] \u0026gt; \u0026#39;9\u0026#39;)){ fprintf(2, \u0026#34;Usage: %s mask command\\n\u0026#34;, argv[0]); exit(1); } if (trace(atoi(argv[1])) \u0026lt; 0) { fprintf(2, \u0026#34;%s: trace failed\\n\u0026#34;, argv[0]); exit(1); } for(i = 2; i \u0026lt; argc \u0026amp;\u0026amp; i \u0026lt; MAXARG; i++){ nargv[i-2] = argv[i]; } nargv[argc-2] = 0; exec(nargv[0], nargv); printf(\u0026#34;trace: exec failed\\n\u0026#34;); exit(0); } attack xv6(moderate) 前置知识(具体的分析，完成下一章project再来看可能会更清晰) 比较源码进行分析，效果更好，建议完成下一章再来看，需要对kfree，kalloc，mappages等函数有一定的理解\nfork时分配物理页面的过程(当前没有写时复制，见kernel/proc.c中的fork())\nnp = allocproc()\n(p-\u0026gt;trapframe = (struct trapframe *)kalloc()\t分配了一页作为trapframe，不同进程有各自独立的trapframe\np-\u0026gt;pagetable = proc_pagetable(p) 创建第一个页表\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 创建第一个页表的时候，就要去映射TRAMPOLINE和TRAPFRAME if(mappages(pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) \u0026lt; 0){ uvmfree(pagetable, 0); return 0; } // TRAMPOLINE存放了处理陷入的汇编代码，空间是内核启动时预先分配好的，物理地址是固定的TRAMPOLINE，这里将用户页表进行映射即可 // map the trapframe page just below the trampoline page, for // trampoline.S. if(mappages(pagetable, TRAPFRAME, PGSIZE, (uint64)(p-\u0026gt;trapframe), PTE_R | PTE_W) \u0026lt; 0){ uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; } // 这两个页面再虚拟地址最顶端相邻两个页，三级页表只需要1个就可以进行映射 xv6系统使用三级页表结构(可以看xv6book中的页表章节进行了解)，所以这里分配了3个页面 uvmcopy(p-\u0026gt;pagetable, np-\u0026gt;pagetable, p-\u0026gt;sz)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { ... for(i = 0; i \u0026lt; sz; i += szinc){\t// sz是用户空间已经分配的空间大小，这里遍历整个用户空间进行复制 szinc = PGSIZE; szinc = PGSIZE; if((pte = walk(old, i, 0)) == 0)\t// 找到用户空间的页表项 panic(\u0026#34;uvmcopy: pte should exist\u0026#34;); if((*pte \u0026amp; PTE_V) == 0) panic(\u0026#34;uvmcopy: page not present\u0026#34;); pa = PTE2PA(*pte);\t// 获取对应的物理地址 flags = PTE_FLAGS(*pte); if((mem = kalloc()) == 0)\t// 分配新的物理地址 goto err; memmove(mem, (char*)pa, PGSIZE);\t// 将就地址内容拷贝到新地址 if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){\t// new为新页表，会创建新页表 kfree(mem); goto err; } } ... } 也就是说，原来父进程有多少用户空间的页面以及页表的页面，子进程就会创建多少页面\n至此，fork分配内存的环节就结束了\nexec调用执行物理页面的变化过程 (见kernel/exec.c)\n(pagetable = proc_pagetable(p))，fork中分析过了，分配了三个页面映射TRAMPOLINE和TRAPFRAME\nexec执行一个新程序，不要分配新的TRAPFRAME吗？exec虽然是执行新程序，但是是在当前程序上执行，使用当前程序分配的TRAPFRAME即可，不需要新分配。换言之，exec不会改变struct proc *p = myproc()，其只会替换用户空间。 注: p以及里面相关地址不会改变，但是内容可能会因为运行新程序而改变。 加载elf中文件的load\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // Load program into memory. for(i=0, off=elf.phoff; i\u0026lt;elf.phnum; i++, off+=sizeof(ph)){ if(readi(ip, 0, (uint64)\u0026amp;ph, off, sizeof(ph)) != sizeof(ph)) goto bad; if(ph.type != ELF_PROG_LOAD) continue; if(ph.memsz \u0026lt; ph.filesz) goto bad; if(ph.vaddr + ph.memsz \u0026lt; ph.vaddr) goto bad; if(ph.vaddr % PGSIZE != 0) goto bad; uint64 sz1; if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0) goto bad; sz = sz1; if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) \u0026lt; 0) goto bad; } exec要执行什么程序，就去看对应程序elf文件中有多少个需要load的页面\nuvmalloc创建用户页面的同时，还会进行映射，创建页表。所以elf文件里面需要load的页面少于512个就会再创建二级和三级页表，也就是2个页面\n512是根据每个页表页表项个数得到的，看xv6book三级页表结构，为2^9\n分配用户栈以及一个保护页面，总计USERSTACK+1个页面，在xv6中#define USERSTACK 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 // Allocate some pages at the next page boundary. // Make the first inaccessible as a stack guard. // Use the rest as the user stack. sz = PGROUNDUP(sz); uint64 sz1; // 只要用户空间的页面没有超过512个，就不会增加新的页表 if((sz1 = uvmalloc(pagetable, sz, sz + (USERSTACK+1)*PGSIZE, PTE_W)) == 0) goto bad; sz = sz1; uvmclear(pagetable, sz-(USERSTACK+1)*PGSIZE); sp = sz; stackbase = sp - USERSTACK*PGSIZE; proc_freepagetable(oldpagetable, oldsz); 释放旧的页表和用户地址空间\n1 2 3 4 5 6 7 8 9 10 void proc_freepagetable(pagetable_t pagetable, uint64 sz) { uvmunmap(pagetable, TRAMPOLINE, 1, 0);\t// 这里不删除物理空间，只删除页表 uvmunmap(pagetable, TRAPFRAME, 1, 0);\t// 同上 // 先解除了TRAMPOLINE和TRAPFRAME映射，这样下面就不会把上面两个物理地址一同删除 // 因为上面两个物理地址是用户页表和内核页表共同使用的 uvmfree(pagetable, sz);\t// 里面uvmunmap最后一个参数是1,说明删除了物理地址空间（都是用户空间） } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // Remove npages of mappings starting from va. va must be // page-aligned. The mappings must exist. // Optionally free the physical memory. void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) { uint64 a; pte_t *pte; int sz; if((va % PGSIZE) != 0) panic(\u0026#34;uvmunmap: not aligned\u0026#34;); // 根据页表遍历删除，说明用户空间的删除是虚拟地址从低到高依次删除 for(a = va; a \u0026lt; va + npages*PGSIZE; a += sz){\tsz = PGSIZE; if((pte = walk(pagetable, a, 0)) == 0) panic(\u0026#34;uvmunmap: walk\u0026#34;); if((*pte \u0026amp; PTE_V) == 0) { printf(\u0026#34;va=%ld pte=%ld\\n\u0026#34;, a, *pte); panic(\u0026#34;uvmunmap: not mapped\u0026#34;); } if(PTE_FLAGS(*pte) == PTE_V) panic(\u0026#34;uvmunmap: not a leaf\u0026#34;); if(do_free){ uint64 pa = PTE2PA(*pte); kfree((void*)pa); } *pte = 0; } } 1 2 3 4 5 6 7 void uvmfree(pagetable_t pagetable, uint64 sz) { if(sz \u0026gt; 0) uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);\t//这里删除了物理地址空间 freewalk(pagetable); } 至此exec页面变化过程就结束了\nxv6底层的内存管理\n通过链表来进行管理，分配的页表从链表头取出来 释放的页面放回到链表头 程序结束后何时回收用户地址空间\n在exit函数中没有找到回收用户地址空间的代码，但是会p-\u0026gt;state = ZOMBIE;通知父进程进行回收\n在wait函数中有这样一段代码freeproc(pp);\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static void freeproc(struct proc *p) { if(p-\u0026gt;trapframe) kfree((void*)p-\u0026gt;trapframe);\t// 先删除trapframe的物理地址空间，这里不回收页表 p-\u0026gt;trapframe = 0; if(p-\u0026gt;pagetable) proc_freepagetable(p-\u0026gt;pagetable, p-\u0026gt;sz);\t// 再删除用户地址空间和页表，这个函数在exec分析中有 p-\u0026gt;pagetable = 0; p-\u0026gt;sz = 0; p-\u0026gt;pid = 0; p-\u0026gt;parent = 0; p-\u0026gt;name[0] = 0; p-\u0026gt;chan = 0; p-\u0026gt;killed = 0; p-\u0026gt;xstate = 0; p-\u0026gt;state = UNUSED; } 也就是说，如果由父进程，用户内存的回收是通过父进程的wait函数来完成的，先回收trapframe的物理内存(不会回收页表)，再从低地址到高地址依次进行回收用户空间页面和页表\n具体实现 页面分配涉及elf文件load，attacktest, secret 和 attack的elf文件中都需要加载多少load展示出来 在xv6目录中执行readelf -l user/_attacktest readelf -l user/_secret readelf -l user/_attack 可以看出attacktest, secret, attack的elf文件需要加载的页面都是两个 attacktest分析（结合前置知识的分析） attacktest的用户页面是通过exec来分配出来的，总共有四个(两个load,一个用户栈，一个guard)，同时需要三级页表来映射 fork为子进程分配空间 三级页表映射到trapframe(一级页表，二级页表，三级页表，trapframe)总共4个页面 复制attacktest的用户页面+二级页表+三级页表进行映射，总共6个页面 前排提示，上面的三级页表和用户页面+两个页表，总共9个页面，后面执行exec会被释放。trapframe不会被释放 子进程执行exec(secret) 新建页面映射trapframe(新一级页表，二级页表，三级页表)，总共3个页面 加载secret的elf文件，总共2个页面，加载同时还要创建二级页表，三级页表映射用户空间(2个页面) 用户栈和guard，总共2个页面 释放上面旧的9个页面，释放9个页面 分配32个页面，在第10个页面上写上secret，申请32个页面 子进程结束，父进程wait，依次释放trapframe页面(1页),用户页面(4 + 32页)，页表页面(5页) 此时空闲链表如下所示 （头，旧的页表页面5页）-\u0026gt; (用户页面32页，secret在第23页上) -\u0026gt; 用户页面(4页，elf的load,用户栈，guard) -\u0026gt; (trapframe) -\u0026gt; \u0026hellip;\u0026hellip;. secret在空闲链表从头开始数第28个页面上 执行pipe(fds) 追踪pipe系统调用的执行流程kernel/sysfile.c中的uint64 sys_pipe(void)函数，我们可以发现pipe执行了pipealloc函数 pipealloc中执行了kalloc，又申请了一个页面。 secret在空闲链表从头开始数第27个页面上 fork为子进程分配空间，和上面一样，总共10个页面 exec(attack)分配空间，和上面一样，到guard页申请结束总共申请了9个 释放fork中的9个页面,与上面申请的9个页面相抵消 27-10 = 17，因此再分配17个页面就可以找到secret attack.c代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/fcntl.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; #include \u0026#34;kernel/riscv.h\u0026#34; int main(int argc, char *argv[]) { // your code here. you should write the secret to fd 2 using write // (e.g., write(2, secret, 8) char secret[8] = {0}; char* end = sbrk(PGSIZE*32); end = end + 16 * PGSIZE; memcpy(secret, end + 32, 8); write(2, secret, 8); exit(1); } 至此，attack的分析就结束了 ","date":"2025-05-21T00:00:00Z","image":"https://zhichenf.github.io/p/xv6-syscall/20_hu_22ba327ac73e8cc8.jpg","permalink":"https://zhichenf.github.io/p/xv6-syscall/","title":"mit6.1810 fall 2024 project2"},{"content":"引言 ​\t最近在学习mit的操作系统课程，本系列博客主要记录课程项目实现的过程及一些思考过程。对应课程网址如下6.1810 / Fall 2024\nsleep(easy) ​\t第一个任务实现用户下的sleep程序，只需调用sleep系统调用即可。sleep.c文件实现如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; int main(int argc, char** argv) { if (argc != 2) { fprintf(2,\u0026#34;Usage: sleep time(s)...\u0026#34;); exit(1); } int time = atoi(argv[1]); sleep(time); exit(0); } pingpong(easy) 前置知识 实验前准备：理解pipe的使用，会linux下pipe系统调用即可。 实现过程 ​\t本任务主要实现了一个父子进程通过管道来进行通信，pingpong.c文件实现如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; int main(int argc, char** argv) { int pipefd[2]; // 用于父进程向子进程发送一字节数据 int pipefd2[2]; // 用户子进程对父进程的回复 pipe(pipefd); pipe(pipefd2); if (0 != fork()) { close(pipefd[0]); close(pipefd2[1]); char buf = \u0026#39;a\u0026#39;; write(pipefd[1],\u0026amp;buf,1); read(pipefd2[0],\u0026amp;buf,1); printf(\u0026#34;%d: received pong\\n\u0026#34;,getpid()); close(pipefd[1]); close(pipefd2[0]); exit(0); } else { close(pipefd[1]); close(pipefd2[0]); char buf = \u0026#39;a\u0026#39;; read(pipefd[0],\u0026amp;buf,1); printf(\u0026#34;%d: received ping\\n\u0026#34;,getpid()); write(pipefd2[1],\u0026amp;buf,1); close(pipefd[0]); close(pipefd2[1]); exit(0); } } primes(moderate)/(hard) 前置知识 前置知识：理解fork系统调用的使用，会linux下fork系统调用即可。 实现过程 ​\t虽然本实验标注了hard，但是主要还是理清筛选素数的过程，理清了过程很快也就能写出来了。首先要理解埃氏筛的过程。埃氏筛是通过每次过滤已经确定的最后一个素数的所有倍数，来完成素数的筛选。\n程序一开始的父进程只需要通过管道向子进程传递所有范围数字即可。 其他进程用于接受父进程的数据来进行过滤，并将过滤后的数据再传给子进程 子进程的过滤及传递过程都是通过primes函数实现，需要接受父进程创建的管道的读端fd作为参数，来接受父进程发送的数据。 同时需要再primes创建一个新的管道并进行fork系统调用，用于该进程过滤后将数据发送给创建的子进程。 下面的任务就是确定每一个进程需要过滤哪些数据 子进程首先会接受父进程发送的第一个数cur，这个第一个cur就是当前确认的素数，进行打印，然后根据这个数cur进行过滤，如果后面接受的父进程的数字不是这个cur的倍数，就可以发送给其子进程；是cur的倍数不发送，完成过滤。 到最后一定是某个子进程只接受了其父进程一个数cur,接受完第一个数后直接关闭文件描述符，其子进程调用read返回0，说明已经将全部素数都打印了，可以结束程序了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; void primes(int) __attribute__((noreturn)); void primes(int in_fd) { int pipefd[2]; pipe(pipefd); int cur; int n = read(in_fd, \u0026amp;cur, sizeof(int)); // 先读确定本次输出 if (0 == n) { // 如果父进程没有发送任何数据 close(in_fd); close(pipefd[0]); close(pipefd[1]); exit(0); } if (fork() != 0) { close(pipefd[0]); printf(\u0026#34;prime %d\\n\u0026#34;,cur); int tmp; while(0 != read(in_fd, \u0026amp;tmp, sizeof(int))) { // 循环获取父进程发送的数据 if (tmp % cur != 0) { write(pipefd[1],\u0026amp;tmp, sizeof(int)); } } close(pipefd[1]); close(in_fd); wait(0); } else { // 子进程只需要保留接受读取父进程数据的fd即可 close(pipefd[1]); close(in_fd); primes(pipefd[0]); // 递归调用子进程 } exit(0); } int main(int argc, char** argv) { int pipefd[2]; pipe(pipefd); if (fork() != 0) { // 主线程将2-280所有数据放入管道左端 close(pipefd[0]); for (int i = 2; i \u0026lt;= 280; ++i) { int tmp = i; write(pipefd[1],\u0026amp;tmp,sizeof(int)); } close(pipefd[1]); wait(0); } else { close(pipefd[1]); primes(pipefd[0]); } } find(moderate) 本实验主要实现查找指定目录树中特定名称的所有文件 前置知识 前置知识，学习user/ls.c中目录读取的内容 fstate根据指定fd获取文件对应信息存放在struct stat数组中 while(read(fd,\u0026amp;de,sizeof(de))) // 循环读取文件夹中的每一项，放到de中，主要存储文件名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // user/ls.c中关键步骤 void ls(char *path) { char buf[512], *p; int fd; struct dirent de; struct stat st; // struct dirent { // ushort inum; // char name[DIRSIZ]; // }; // struct stat { // int dev; // File system\u0026#39;s disk device // uint ino; // Inode number // short type; // Type of file // short nlink; // Number of links to file // uint64 size; // Size of file in bytes // }; if((fd = open(path, O_RDONLY)) \u0026lt; 0){\t// 打开文件 fprintf(2, \u0026#34;ls: cannot open %s\\n\u0026#34;, path); return; } if(fstat(fd, \u0026amp;st) \u0026lt; 0){\t// 获取文件信息 fprintf(2, \u0026#34;ls: cannot stat %s\\n\u0026#34;, path); close(fd); return; } switch(st.type){\t// 判断文件类型，根据不同类型进行不同操作 case T_DEVICE: case T_FILE:\t// 设备文件或普通文件直接输出对应信息 ls 文件 printf(\u0026#34;%s %d %d %d\\n\u0026#34;, fmtname(path), st.type, st.ino, (int) st.size); break; case T_DIR:\t// ls 目录 if(strlen(path) + 1 + DIRSIZ + 1 \u0026gt; sizeof buf){ printf(\u0026#34;ls: path too long\\n\u0026#34;); break; } strcpy(buf, path); p = buf+strlen(buf); *p++ = \u0026#39;/\u0026#39;; while(read(fd, \u0026amp;de, sizeof(de)) == sizeof(de)){\t// 循环读取文件夹里的每一个文件 if(de.inum == 0) continue; memmove(p, de.name, DIRSIZ);\t// 拼接文件路径 p[DIRSIZ] = 0;\t// 字符串末尾\u0026#39;\\0\u0026#39; if(stat(buf, \u0026amp;st) \u0026lt; 0){\t// 获取文件信息 printf(\u0026#34;ls: cannot stat %s\\n\u0026#34;, buf); continue; } printf(\u0026#34;%s %d %d %d\\n\u0026#34;, fmtname(buf), st.type, st.ino, (int) st.size); } break; } close(fd); } 实现过程 学会了ls中的相关操作，就可以来完成find操作了 find操作主要是在文件树中进行一个深度优先遍历(dfs)来查找，找到对应文件信息符合要求就可以进行打印 前面模仿ls中的操作来打开对应的目录，注意find命令接受的第一个参数必须是文件夹路径 while循环根据每一个子文件名(完整路径的文件名，需路径拼接)调用sate来获取文件信息，路径拼接和ls中一样 注意判断子文件名(非完整路径，无需拼接)要排除\u0026quot;.\u0026ldquo;和\u0026rdquo;..\u0026quot; 子文件还是目录，进行递归调用即可；是文件比较是否是要查找的，是就输出即可。 ​\tfind.c文件如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; #include \u0026#34;kernel/fs.h\u0026#34; #include \u0026#34;kernel/fcntl.h\u0026#34; void find(char* dir_path, char* file_name) { char buf[512], *p; int fd; struct dirent de; struct stat st; if ((fd = open(dir_path, O_RDONLY)) \u0026lt; 0) { fprintf(2, \u0026#34;find: cannot open %s\\n\u0026#34;, dir_path); return; } if (fstat(fd, \u0026amp;st) \u0026lt; 0) { fprintf(2,\u0026#34;find: cannot stat %s\\n\u0026#34;, dir_path); close(fd); return; } if (T_DIR != st.type) { char err_msg[128] = \u0026#34;you must find in directory!\u0026#34;; write(3, err_msg, strlen(err_msg) + 1); } if(strlen(dir_path) + 1 + DIRSIZ + 1 \u0026gt; sizeof(buf)) { printf(\u0026#34;find: path too long\\n\u0026#34;); close(fd); exit(-1); } // 找dir_path中名字为file_name的文件 strcpy(buf,dir_path); p = buf + strlen(buf); *p++ = \u0026#39;/\u0026#39;; while(read(fd, \u0026amp;de, sizeof(de)) == sizeof(de)) { if (de.inum == 0 || strcmp(de.name, \u0026#34;.\u0026#34;) == 0 || strcmp(de.name, \u0026#34;..\u0026#34;) == 0) { continue; } memmove(p, de.name, DIRSIZ); p[DIRSIZ] = 0; if (stat(buf, \u0026amp;st) \u0026lt; 0) { printf(\u0026#34;find: cannot stat %s\\n\u0026#34;,buf); continue; } if (T_DIR == st.type) { find(buf, file_name); } else { if (strcmp(de.name,file_name) == 0) { printf(\u0026#34;%s\\n\u0026#34;,buf); } } } } int main(int argc, char** argv) { if (argc \u0026lt; 3) { char err_msg[32] = \u0026#34;argv error\u0026#34;; write(3, err_msg, strlen(err_msg) + 1); exit(0); } find(argv[1], argv[2]); } xargs(moderate) 前置知识 理解文件重定向，以及fork 和 exec中间进行文件重定向的操作。这一块xv6 book中应该有很好的解释。\n你需要实现xargs.c文件，因此你需要知道操作系统是如何调用你写的这个程序\n理解匿名管道的调用过程，在echo hello too | xargs echo bye 中操作系统是如何执行xargs程序的\n在user/sh.c中找到下面这些代码\n1 2 3 4 5 6 7 8 9 10 // 一些数据结构 struct pipecmd { int type; struct cmd *left; struct cmd *right; }; struct cmd { int type; }; 在这个实验里，我们只需要知道sh根据用户输入的命令，将命令解析为struct cmd结构体 当cmd是一个pipecmd的时候，里面有左右两个cmd分别代表了 管道|左右两个命令 如echo hello too | xargs echo bye 中， cmd-\u0026gt;left 对应echo hello too解析出来的命名 ，cmd-\u0026gt;right 对应 xargs echo bye 解析出来的命令。 随后会调用fork函数来进行重定向 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void runcmd(struct cmd *cmd) { switch(cmd-\u0026gt;type){\t...... case PIPE: pcmd = (struct pipecmd*)cmd;\t// 恩哼？有点像多态 // sh.c中还有很多struct othername_cmd{}的结构体 // 只需要保证前四个字节都是用来标识type类型，我们就可以强制类型转换 // 将公共sturct cmd转换为你所需要的struct类型 if(pipe(p) \u0026lt; 0) panic(\u0026#34;pipe\u0026#34;); if(fork1() == 0){\tclose(1);\t// 第一个子进程关闭终端的标准输出 dup(p[1]);\t// 标准输出被重定向到管道写端 close(p[0]); close(p[1]); runcmd(pcmd-\u0026gt;left);\t// 执行左边命令，原来的标准输出会写道管道写端 } if(fork1() == 0){ close(0);\t// 第二个子进程关闭终端的标准输入 dup(p[0]);\t// 标准输入被重定向到了管道读端 close(p[0]); close(p[1]); runcmd(pcmd-\u0026gt;right);\t// 执行右命令，原来应该从标准输入读的参数会从管道读端里读 } close(p[0]); close(p[1]); wait(0); wait(0); break; ...... } } 理解了过程，就可以知道xargs程序的执行会将左命令生成的标准输出追加到其指定要执行程序的参数后面 实现过程 1 find . b | xargs grep hello 以上面的命令为例，对于find . b 找到的每一个文件， 都要指定一次 grep hello b，因此xargs是实现可以如下所示 首先循环标准输入，解析标准输入，当标准输入已经读完一行就可以开始执行grep命令了 调用fork创建子进程，子进程将获得的那一行标准输入添加到grep的参数后面，中调用exec系统调用执行grep命令 父进程用于恢复之前读取的buf缓存 xargs.c 文件夹如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; #include \u0026#34;kernel/fs.h\u0026#34; #include \u0026#34;kernel/fcntl.h\u0026#34; #include \u0026#34;kernel/param.h\u0026#34; int main(int argc, char** argv) { char buf[1024] = {0}; int offset = 0; while(read(0,buf + offset,1)) { int is_end = 0; if (buf[offset] == \u0026#39;\\n\u0026#39; || buf[offset] == \u0026#39;\\0\u0026#39;) { if (buf[offset] == \u0026#39;\\0\u0026#39;) { is_end = 1; } buf[offset] = \u0026#39;\\0\u0026#39;; // buf中已经读取了一行数据，子进程将buf追加到参数中去，执行exec if (0 != fork()) { wait((void*) 0); offset = 0; memset(buf,0,sizeof(buf)); continue; } else { char* new_argv[MAXARG]; int index = 0; new_argv[index++] = argv[1]; for (int i = 2; i \u0026lt; argc; ++i) { new_argv[index++] = argv[i]; } new_argv[index++] = buf; new_argv[index] = (void*) 0; exec(argv[1], new_argv); exit(0); } } if (is_end == 1) { break; } ++offset; } } ","date":"2025-05-20T00:00:00Z","image":"https://zhichenf.github.io/p/xv6-util/20_hu_26381f7806c92aa6.jpg","permalink":"https://zhichenf.github.io/p/xv6-util/","title":"mit6.1810 fall 2024 project1"},{"content":"引言 CUDA（Compute Unified Device Architecture）是 NVIDIA 推出的并行计算平台和编程模型，允许开发者利用 GPU（图形处理器）进行高性能通用计算（GPGPU）。它扩展了 C/C++ 等语言，使开发者能够编写直接在 GPU 上运行的代码，大幅加速计算密集型任务。\nCUDA 核心概念 (1) 主机（Host）与设备（Device） Host（主机）：CPU 及其内存（如 malloc 分配的内存）。 Device（设备）：GPU 及其显存（如 cudaMalloc 分配的内存）。 数据传输：主机和设备之间需要通过 cudaMemcpy 显式传输数据。 (2) 核函数（Kernel） GPU 上并行执行的函数，用\n1 __global__ void(...) {} 1 2 3 4 __global__ void addKernel(int *a, int *b, int *c) { int i = threadIdx.x; c[i] = a[i] + b[i]; } 调用方式：\n1 \u0026lt;\u0026lt;\u0026lt;grid, block\u0026gt;\u0026gt;\u0026gt; 指定并行执行配置：\n1 addKernel\u0026lt;\u0026lt;\u0026lt;1, N\u0026gt;\u0026gt;\u0026gt;(d_a, d_b, d_c); // 1个block，N个threads (3) 线程层次 线程（Thread）：最基本的执行单元。 线程块（Block）：一组线程，可共享 __shared__ 内存，可同步（__syncthreads()）。 网格（Grid）：多个线程块的集合。 索引计算： threadIdx.x：线程在 block 内的索引。 blockIdx.x：block 在 grid 内的索引。 blockDim.x：block 的大小（线程数）。 CUDA 执行流程 分配设备内存\n1 2 int *d_a; cudaMalloc(\u0026amp;d_a, size); 主机→设备数据传输\n1 cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice); 启动核函数\n1 kernel\u0026lt;\u0026lt;\u0026lt;grid, block\u0026gt;\u0026gt;\u0026gt;(d_a, d_b, d_c); 设备→主机数据传输\n1 cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost); 释放设备内存\n1 cudaFree(d_a); 向量加法(一维)执行流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026#34;cuda_runtime.h\u0026#34; #include \u0026#34;device_launch_parameters.h\u0026#34; #define cudaCheckError(msg) \\ do { \\ cudaError_t __err = cudaGetLastError(); \\ if (__err != cudaSuccess) { \\ fprintf(stderr, \u0026#34;Fatal error: %s (%s at %s:%d)\\n\u0026#34;, \\ msg, cudaGetErrorString(__err), \\ __FILE__, __LINE__); \\ fprintf(stderr, \u0026#34;*** FAILED - ABORTING\\n\u0026#34;); \\ exit(1); \\ } \\ } while(0) const int DSIZE = 4096; __global__ void vector_add(const int* a, const int* b, int* c, int size) { int index = blockIdx.x * blockDim.x + threadIdx.x;\t// 确定唯一id if (index \u0026lt; size) { c[index] = a[index] + b[index]; } } int main() { std::vector\u0026lt;int\u0026gt; v1 = { 1,2,3,4,5 }; std::vector\u0026lt;int\u0026gt; v2 = { 10,20,30,40,50 }; std::vector\u0026lt;int\u0026gt; v3 = { 0,0,0,0,0 }; int* d_a, * d_b, * d_c;\t// device数据拷贝 cudaMalloc((void**)\u0026amp;d_a, v1.size() * sizeof(int)); cudaMalloc((void**)\u0026amp;d_b, v2.size() * sizeof(int)); cudaMalloc((void**)\u0026amp;d_c, v3.size() * sizeof(int)); cudaCheckError(\u0026#34;cudaMalloc failuer\u0026#34;); cudaMemcpy(d_a, v1.data(), v1.size() * sizeof(int), cudaMemcpyHostToDevice); cudaMemcpy(d_b, v2.data(), v2.size() * sizeof(int), cudaMemcpyHostToDevice); cudaCheckError(\u0026#34;cudaMemcpy failuer\u0026#34;); vector_add\u0026lt;\u0026lt;\u0026lt;(v1.size() + 512 - 1) / 512, 512\u0026gt;\u0026gt;\u0026gt;(d_a, d_b, d_c, v1.size()); cudaCheckError(\u0026#34;vector_add failuer\u0026#34;); cudaMemcpy(v3.data(), d_c, v3.size() * sizeof(int), cudaMemcpyDeviceToHost); cudaCheckError(\u0026#34;cudaMemcpy from device failuer\u0026#34;); for (auto v : v3) { std::cout \u0026lt;\u0026lt; v \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; // 手动释放 cudaFree(d_a); cudaFree(d_b); cudaFree(d_c); } 通过RAII管理device内存，同时实现模板支撑更多的数据类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026#34;cuda_runtime.h\u0026#34; #include \u0026#34;device_launch_parameters.h\u0026#34; #define cudaCheckError(msg) \\ do { \\ cudaError_t __err = cudaGetLastError(); \\ if (__err != cudaSuccess) { \\ fprintf(stderr, \u0026#34;Fatal error: %s (%s at %s:%d)\\n\u0026#34;, \\ msg, cudaGetErrorString(__err), \\ __FILE__, __LINE__); \\ fprintf(stderr, \u0026#34;*** FAILED - ABORTING\\n\u0026#34;); \\ exit(1); \\ } \\ } while(0) const int DSIZE = 4096; __global__ void vector_add(const int* a, const int* b, int* c, int size) { int index = blockIdx.x * blockDim.x + threadIdx.x; if (index \u0026lt; size) { c[index] = a[index] + b[index]; } } // RAII进行管理,管理device的内存 // 通过模板支持更多数据类型 template\u0026lt;typename T\u0026gt; class DeviceData { public: DeviceData(size_t size) { cudaMalloc((void**)\u0026amp;data_, size); } ~DeviceData() { cudaFree(data_); } int* getData() { return data_; } private: T* data_; }; int main() { std::vector\u0026lt;int\u0026gt; v1 = { 1,2,3,4,5 }; std::vector\u0026lt;int\u0026gt; v2 = { 10,20,30,40,50 }; std::vector\u0026lt;int\u0026gt; v3 = { 0,0,0,0,0 }; DeviceData\u0026lt;int\u0026gt; d_a(v1.size() * sizeof(int)); DeviceData\u0026lt;int\u0026gt; d_b(v2.size() * sizeof(int)); DeviceData\u0026lt;int\u0026gt; d_c(v3.size() * sizeof(int)); cudaCheckError(\u0026#34;cudaMalloc failuer\u0026#34;); cudaMemcpy(d_a.getData(), v1.data(), v1.size() * sizeof(int), cudaMemcpyHostToDevice); cudaMemcpy(d_b.getData(), v2.data(), v2.size() * sizeof(int), cudaMemcpyHostToDevice); cudaCheckError(\u0026#34;cudaMemcpy failuer\u0026#34;); vector_add\u0026lt;\u0026lt;\u0026lt;(v1.size() + 512 - 1) / 512, 512\u0026gt;\u0026gt;\u0026gt;(d_a.getData(), d_b.getData(), d_c.getData(), v1.size()); cudaCheckError(\u0026#34;vector_add failuer\u0026#34;); cudaMemcpy(v3.data(), d_c.getData(), v3.size() * sizeof(int), cudaMemcpyDeviceToHost); cudaCheckError(\u0026#34;cudaMemcpy from device failuer\u0026#34;); for (auto v : v3) { std::cout \u0026lt;\u0026lt; v \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; } 矩阵乘法（多维）执行流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include \u0026#34;cuda_runtime.h\u0026#34; #include \u0026#34;device_launch_parameters.h\u0026#34; #include \u0026lt;iostream\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define cudaCheckError(msg) \\ do { \\ cudaError_t __err = cudaGetLastError(); \\ if (__err != cudaSuccess) { \\ fprintf(stderr, \u0026#34;Fatal error: %s (%s at %s:%d)\\n\u0026#34;, \\ msg, cudaGetErrorString(__err), \\ __FILE__, __LINE__); \\ fprintf(stderr, \u0026#34;*** FAILED - ABORTING\\n\u0026#34;); \\ exit(1); \\ } \\ } while(0) const int dy = 2;\tconst int dx = 3; // A 是dy * dx, B 是dx * dy, C 是dy * dy // C = AB 实现 __global__ void mmul(const float* A, const float* B, float* C, int dy, int dx) { // m * k 和 k * n 矩阵乘法的通用写法 int m = dy, k = dx, n = dy; // 计算当前线程对应的行和列 // 可以认为，只用row和col能用来表示C中元素的线程才会进行计算 // 不会发生线程的冗余计算 int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; // C[row][col] 为 A的第row行 * B的第col列 if (row \u0026lt; m \u0026amp;\u0026amp; col \u0026lt; n) { float sum = 0.0f; for (int i = 0; i \u0026lt; k; i++) { sum += A[row * k + i] * B[i * n + col]; // A 行主序，B 列主序 } C[row * n + col] = sum; // C 行主序 } } int main() { float* h_A, * h_B, * h_C, * d_A, * d_B, * d_C; h_A = new float[dy * dx]; h_B = new float[dx * dy]; h_C = new float[dy * dy]; h_A[0] = 1.0f; h_A[1] = 2.0f; h_A[2] = 1.0f; h_A[3] = 2.0f; h_A[4] = 2.0f; h_A[5] = 1.0f; h_B[0] = 1.0f; h_B[1] = 2.0f; h_B[2] = 2.0f; h_B[3] = 1.0f; h_B[4] = 2.0f; h_B[5] = 2.0f; cudaMalloc((void**)\u0026amp;d_A, dy * dx * sizeof(float)); cudaMalloc((void**)\u0026amp;d_B, dx * dy * sizeof(float)); cudaMalloc((void**)\u0026amp;d_C, dy * dy * sizeof(float)); cudaCheckError(\u0026#34;cudaMalloc failuer\u0026#34;); cudaMemcpy(d_A, h_A, dy * dx * sizeof(float), cudaMemcpyHostToDevice); cudaMemcpy(d_B, h_B, dx * dy * sizeof(float), cudaMemcpyHostToDevice); cudaCheckError(\u0026#34;cudaMemcpy error\u0026#34;); // 设备大小 dim3 block(2, 2);\t// 每个block有2*2个线程 // 通用公式 // grid((结果矩阵列数 + block.x - 1) / block.x, //\t(结果矩阵行数 + block.y - 1) / block.y); dim3 grid((dy + block.x - 1) / block.x, (dy + block.y - 1) / block.y);\t// 用来定义grid中block的数量 mmul \u0026lt;\u0026lt; \u0026lt;grid, block \u0026gt;\u0026gt; \u0026gt; (d_A, d_B, d_C, dy, dx); cudaCheckError(\u0026#34;kernel launch failure\u0026#34;); cudaMemcpy(h_C, d_C, dy * dy * sizeof(float), cudaMemcpyDeviceToHost); for (int i = 0; i \u0026lt; 4; ++i) { std::cout \u0026lt;\u0026lt; h_C[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; if ((i + 1) % 2 == 0) { std::cout \u0026lt;\u0026lt; std::endl; } } cudaFree(d_A); cudaFree(d_B); cudaFree(d_C); delete[] h_A; delete[] h_B; delete[] h_C; } 注意事项 在对host主机的代码中，可以使用标准库，包括shared_ptr, unique_ptr等；但是在核函数当中，对device设备编程，则无法使用标准库里的东西。 cuda的device可以支持模板以实现更通用的功能 可以自己实现RAII对device的资源进行管理 ","date":"2025-05-12T00:00:00Z","image":"https://zhichenf.github.io/p/cuda_c_basic/1_hu_80c0fe1b049a4c41.jpg","permalink":"https://zhichenf.github.io/p/cuda_c_basic/","title":"cuda_c_basic"},{"content":"引言 ​\t今天看了Cpp con 2014 - Walter E. Brown博士讲的有关模板元编程的讲座，感觉打开了新世界的大门，以前虽然也看了些和模板有关的代码，但一直没有系统的了解过，而今天看完讲座，感觉串通了很多东西，也开始了解模板元编程。所以写者想在这里记录一些讲座上的代码，以及自己的一些思考。\n模板元编程(TMP) ​\t普通的编程操作的对象都是数据，而在模板元编程当中，我们可以对类型进行操作。就拿普通函数和元函数进行比较，元函数不在是一个普通函数的形式，通常是一个模板类。普通函数的参数对应了元函数的函数模板，操作的对象也由数据变成了类型。普通函数的返回值与元函数中通常定义公开的type类型和value值形成了对应。\n模板也可以递归 ​\t以前一直以为，递归只能再函数中实现，没想到也能在模板中进行递归。不过再一想，这里用到都是元函数，那么可以用递归也合理了很多。下面两个例子展现了两种递归的实现。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 namespace yc { // 计算gcd // 在主模板中调用了递归，特化作为递归出口 template \u0026lt;unsigned M, unsigned N\u0026gt; struct gcd { static constexpr int value = gcd\u0026lt;N,M%N\u0026gt;::value; }; template\u0026lt;unsigned M\u0026gt; struct gcd\u0026lt;M,0\u0026gt; { static_assert(M != 0); static constexpr int value = M; }; // rank, rank的主要作用是返回数组的维度，rank\u0026lt;int[10][20][30]\u0026gt;::value 的值是3 // 在特化中调用了递归，主模板作为递归出口 template\u0026lt;typename T\u0026gt; struct rank { static constexpr size_t value = 0u;}; template\u0026lt;typename U, size_t N\u0026gt; struct rank\u0026lt;U[N]\u0026gt; { static size_t const value = 1u + rank\u0026lt;U\u0026gt;::value; }; } ::type ​\t模板元编程的操作对象是类型，下面就来看看对类型的操作吧。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 namespace yc{ // remove_const template\u0026lt;typename T\u0026gt; struct remove_const {using type = T;}; template\u0026lt;typename T\u0026gt; struct remove_const\u0026lt;T const\u0026gt; {using type = T;};\t// 特化去除类型的const属性，下面去除volatile也是如此 // 一些元函数可以继承type_is, 保证元函数的一些规范，一定会有名为type的类型 template\u0026lt;typename T\u0026gt; struct type_is { using type = T;}; template\u0026lt;typename T\u0026gt; struct remove_volatile : type_is\u0026lt;T\u0026gt; {}; template\u0026lt;typename U\u0026gt; struct remove_volatile\u0026lt;U volatile\u0026gt; : type_is\u0026lt;U\u0026gt; {}; // IF编译器决定调用哪一个,因为是模板，所以T和F可以是任意类型，包括函数，类。 // 可以通过这种方式，让编译器选择将要执行的函数类型，以及将要继承的基类。 template\u0026lt;bool, typename T, typename\u0026gt; struct IF : type_is\u0026lt;T\u0026gt; {}; template\u0026lt;typename T, typename F\u0026gt; struct IF\u0026lt;false,T,F\u0026gt; : type_is\u0026lt;F\u0026gt; {}; } SFINAE ​ 模板实例化需要经过替换的过程，在替换的过程中，如果失败，并不是错误，而是会放弃这次替换。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 namespace yc{\t// enable_if， SFINAE的实用工具 template\u0026lt;bool, typename T = void\u0026gt; struct enable_if : type_is\u0026lt;T\u0026gt; {}; template\u0026lt;typename T\u0026gt; struct enable_if\u0026lt;false,T\u0026gt; {}; // 当传入为false的时候，不给::type，用来后面enable_if_t发生SFINAE template\u0026lt;bool b, typename T\u0026gt; using enable_if_t = typename enable_if\u0026lt;b, T\u0026gt;::type;\t// 通常后缀_t 表示 ::type //SFINAE // 当条件是false的时候，enable_if_t会发生错误，因为enable_if没有type类型，发生SFINAE template\u0026lt;typename T\u0026gt; enable_if_t\u0026lt;is_integral_v\u0026lt;T\u0026gt;, size_t\u0026gt; f(T val) { return val; } template\u0026lt;typename T\u0026gt; enable_if_t\u0026lt;is_floating_point_v\u0026lt;T\u0026gt;, long double\u0026gt; f(T val) { return val; }; } ::value 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 namespace yc{ // ::value template\u0026lt;typename T, T v\u0026gt; struct integral_constant {\t// 这里T类型位置，考虑换一个名字 static constexpr T value = v; // 允许将integral_constant 转为T类型 或者调用获得类型T constexpr explicit operator T() const noexcept { return value; } constexpr T operator()() const noexcept { return value; } }; // 重构rank template\u0026lt;typename T\u0026gt; struct rank2 : integral_constant\u0026lt;size_t, 0u\u0026gt; {}; template\u0026lt;typename U, size_t N\u0026gt; struct rank2\u0026lt;U[N]\u0026gt; : integral_constant\u0026lt;size_t, 1u + rank2\u0026lt;U\u0026gt;::value\u0026gt; {}; // 之前的rank没有考虑过的情况 template\u0026lt;typename U\u0026gt; struct rank2\u0026lt;U[]\u0026gt; : integral_constant\u0026lt;size_t, 1u + rank2\u0026lt;U\u0026gt;::value\u0026gt; {}; // 一些模板aliases template\u0026lt;bool b\u0026gt; using bool_constant = integral_constant\u0026lt;bool, b\u0026gt;; using true_type = bool_constant\u0026lt;true\u0026gt;; using false_type = bool_constant\u0026lt;false\u0026gt;; } 更多的类型判断 ​\t在这里补充一下模板实例化中有关特化的执行过程\n​\t在实例化中，编译器会根据主模板，将所有类型确定下来，然后去特化中找严格匹配的类型，如果特化中没有严格匹配的类型，就不会走特化版本，而是走主模板的版本。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 namespace yc{ // 判断一个类型是否是void类型 // 写者在这里进行了总结，一般主模板都给一个false的value，在特化中将满足条件的特化给予一个为true的value template\u0026lt;typename T\u0026gt; struct is_void : false_type {}; template\u0026lt;\u0026gt; struct is_void\u0026lt;void\u0026gt; : true_type {}; template\u0026lt;\u0026gt; struct is_void\u0026lt;void const\u0026gt; : true_type {}; template\u0026lt;\u0026gt; struct is_void\u0026lt;void volatile\u0026gt; : true_type {}; template\u0026lt;\u0026gt; struct is_void\u0026lt;void volatile const\u0026gt; : true_type {}; // 判断两个类型是否相等 template\u0026lt;typename T, typename U\u0026gt; struct is_same : false_type {}; template\u0026lt;typename T\u0026gt; struct is_same\u0026lt;T,T\u0026gt; : true_type {}; template\u0026lt;typename T\u0026gt; using remove_cv = remove_volatile\u0026lt;remove_const_t\u0026lt;T\u0026gt;\u0026gt;; template\u0026lt;typename T\u0026gt; using remove_cv_t = typename remove_cv\u0026lt;T\u0026gt;::type; // 重构is_void2 template\u0026lt;typename T\u0026gt; using is_void2 = is_same\u0026lt;remove_cv_t\u0026lt;T\u0026gt;, void\u0026gt;; // 判断类型是不是后面类型列表里面的类型 template\u0026lt;typename T, typename... P0ToN\u0026gt; struct is_one_of;\t// 主模板声明，用来确定类型，而下面的特化都一定能走到，所以不用写主模板 template\u0026lt;typename T\u0026gt; struct is_one_of\u0026lt;T\u0026gt; : false_type {}; template\u0026lt;typename T, typename... P1ToN\u0026gt; struct is_one_of\u0026lt;T,T,P1ToN...\u0026gt; : true_type {}; template\u0026lt;class T, typename P0, typename... P1toN\u0026gt; struct is_one_of\u0026lt;T, P0, P1toN...\u0026gt; : is_one_of\u0026lt;T, P1toN...\u0026gt; {}; // 模板参数列进行递归操作很常见，但这里尽然进行了一个递归的继承，递归也能玩这么六，太精彩了~~~ // 重构is_void3 template\u0026lt;typename T\u0026gt; using is_void3 = is_one_of\u0026lt;T, void, void const, void volatile, void volatile const\u0026gt;; } decltype和declval 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace yc { // decltype 和 declval // declval 假装给了这个类型的一个值。 // 下面假装是一个函数调用，但是是编译期并不会实际执行函数调用，只是为了找到这个函数来获取返回值类型。 template\u0026lt;typename T\u0026gt; decltype(foo(declval\u0026lt;T\u0026gt;())) t; // declval\u0026lt;T\u0026amp;\u0026gt;() 给的是一个右值 // is_copy_assignable template\u0026lt;class T\u0026gt; struct is_copy_assignable { private: template\u0026lt;typename U, typename = decltype(declval\u0026lt;U\u0026amp;\u0026gt;() = declval\u0026lt;U const\u0026amp;\u0026gt;())\u0026gt; static true_type try_assignment(U\u0026amp;\u0026amp;);\t// 可能会触发SFINAE static false_type try_assignment(...); public: using type = decltype(try_assignment(declval\u0026lt;T\u0026gt;())); }; } void_t 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 namespace yc{ // void_t template\u0026lt;typename...\u0026gt; using void_t = void; // 查看元函数是否有type成员 // 为什么typename=void 必须是void, 改成int会发生什么？ // 改成int,编译器先看主模板，实例化为has_type_member\u0026lt;SomeType, int\u0026gt;，再尝试匹配偏特化会失败。 // 特化类型必须精确匹配，才能走特化版本，否则不会 // 如果想要改成int,上面的也要改成void_t = int template\u0026lt;typename, typename = void\u0026gt; // 这里的void一定要和void_t里的类型相匹配，选择void是一种实现方式 struct has_type_member : false_type {}; // 类型匹配优先走偏特化，即使这两个看起来一样 template\u0026lt;typename T\u0026gt; struct has_type_member\u0026lt;T,void_t\u0026lt;typename T::type\u0026gt;\u0026gt; : true_type{}; // is_copy_assignable优化 template\u0026lt;typename T\u0026gt; using copy_assignable_t = decltype(declval\u0026lt;T\u0026amp;\u0026gt;() = declval\u0026lt;T const\u0026amp;\u0026gt;()); // T const\u0026amp; -\u0026gt; T\u0026amp;\u0026amp; 就可以实现能否进行移动的判断 template\u0026lt;typename T, typename = void\u0026gt; struct is_copy_assignable2 : false_type {}; template\u0026lt;typename T\u0026gt; struct is_copy_assignable2\u0026lt;T, void_t\u0026lt;copy_assignable_t\u0026lt;T\u0026gt;\u0026gt;\u0026gt; : is_same\u0026lt;copy_assignable_t\u0026lt;T\u0026gt;,T\u0026amp;\u0026gt;{}; // 拷贝后的类型应当是T\u0026amp;, 所以需要和T\u0026amp;比较是否类型相等 } ","date":"2025-04-10T00:00:00Z","image":"https://zhichenf.github.io/p/open_the_door_of_the_tmp/26_hu_e92c25be12210b03.jpg","permalink":"https://zhichenf.github.io/p/open_the_door_of_the_tmp/","title":"打开模板元编程的大门"},{"content":"引言 线程屏障（Thread Barrier）是一种同步机制，用于让一组线程在某个点上等待，直到所有线程都到达该点后再继续执行。屏障通常用于多线程编程中，确保所有线程在某个阶段完成后再进入下一个阶段。\nstd::latch ​\t在了解barrier之间，让我们先来看一看std::latch吧。\n构造函数explicit latch(std::ptrdiff_t expected); 传入一个参数，指定计数器初始的值\n成员函数 count_down(std::ptrdiff_t n = 1); 每次执行让计数器减去n，默认为1\n成员函数 wait(); 等待计数器减为0\n成员函数 try_wait(); 非阻塞检查计数器是否减为0，为0则返回true，极少数情况下也会返回false\n在多线程环境中，如果多个线程同时操作 latch 的计数器，可能会出现竞争条件。例如：\n线程 A 调用 count_down() 将计数器减到 0。 线程 B 在计数器减到 0 的瞬间调用 try_wait()，但由于线程调度或内存可见性问题，线程 B 看到的计数器值可能仍然不为 0。 这种情况下，try_wait 可能会返回 false，即使计数器实际上已经减到 0。\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include \u0026lt;iostream\u0026gt; #include \u0026lt;latch\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;chrono\u0026gt; #include \u0026lt;random\u0026gt; void func(std::latch\u0026amp; latch) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution\u0026lt;\u0026gt; dis(1, 10); std::this_thread::sleep_for(std::chrono::seconds(dis(gen))); std::cout \u0026lt;\u0026lt; \u0026#34; finish work \u0026#34; \u0026lt;\u0026lt; std::endl; latch.count_down(); } int main() { std::latch latch(3); std::vector\u0026lt;std::thread\u0026gt; threads; for (int i = 0; i \u0026lt; 3; i++) { threads.emplace_back(func, std::ref(latch)); } latch.wait();\t// 会等待latch减为0，才会继续运行下去 std::cout \u0026lt;\u0026lt; \u0026#34;all work is done\u0026#34; \u0026lt;\u0026lt; std::endl; for (auto\u0026amp; thread : threads) { thread.join(); } return 0; } std::barrier std::barrier 是 C++20 中引入的一个同步原语，用于让一组线程在某个点上等待，直到所有线程都到达该点后再继续执行。与 std::latch 不同，std::barrier 是 可重用的，适用于多阶段任务的同步。\n类别 函数签名 描述 构造函数 explicit barrier(std::ptrdiff_t count); 构造一个 barrier，指定需要等待的线程数 count。 template\u0026lt;class CompletionFunction\u0026gt; barrier(std::ptrdiff_t count, CompletionFunction\u0026amp;\u0026amp; completion); 构造一个 barrier，指定线程数 count 和完成回调函数 completion。 成员函数 arrival_token arrive( std::ptrdiff_t n = 1 ); 通知 barrier 当前线程已到达，update 指定减少的计数（默认 1）。 void wait( arrival_token\u0026amp;\u0026amp; arrival ) const; 等待计数器减为0 void arrive_and_wait(); 通知 barrier 当前线程已到达，并阻塞直到所有线程到达。 void arrive_and_drop(); 通知 barrier 当前线程已到达，并减少 barrier 的计数（线程退出）。 代码案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;barrier\u0026gt; #include \u0026lt;latch\u0026gt; #include \u0026lt;chrono\u0026gt; #include \u0026lt;random\u0026gt; void func(std::barrier\u0026lt;\u0026gt; \u0026amp; barrier) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution\u0026lt;\u0026gt; dis(1, 10); std::this_thread::sleep_for(std::chrono::seconds(dis(gen))); std::cout \u0026lt;\u0026lt; \u0026#34; start work\u0026#34; \u0026lt;\u0026lt; std::endl; barrier.arrive_and_wait(); std::this_thread::sleep_for(std::chrono::seconds(dis(gen))); std::cout \u0026lt;\u0026lt; \u0026#34; end work\u0026#34; \u0026lt;\u0026lt; std::endl; barrier.arrive_and_wait(); } int main() { std::barrier\u0026lt;\u0026gt; barrier(4); std::vector\u0026lt;std::thread\u0026gt; threads; for (int i = 0; i \u0026lt; 3; i++) { threads.emplace_back(func, std::ref(barrier)); } barrier.arrive_and_wait(); std::cout \u0026lt;\u0026lt; \u0026#34;all people ready\u0026#34; \u0026lt;\u0026lt; std::endl; barrier.wait(barrier.arrive());\t// 相当于arrive_and_wait() std::cout \u0026lt;\u0026lt; \u0026#34;all work is done\u0026#34; \u0026lt;\u0026lt; std::endl; for (auto\u0026amp; thread : threads) { thread.join(); } return 0; } ","date":"2025-03-19T00:00:00Z","image":"https://zhichenf.github.io/p/latch_and_barrier/17_hu_458f0a92d1a8943f.jpg","permalink":"https://zhichenf.github.io/p/latch_and_barrier/","title":"线程屏障"},{"content":"引言 奇异递归模板模式（Curiously Recurring Template Pattern，简称 CRTP），也被称为奇异模板，是 C++ 中的一种模板编程技术。它允许基类使用派生类的类型信息，实现了一种编译时多态。\n奇异递归模板模式 ​\t奇异递归模板是一种利用模板技术实现的编译时多态，允许一个类在调用函数的时候实现不同的方法。\n实现方法：\n基类使用模板类，模板参数代表了派生类类型 函数里将基类指针(this)转为派生类指针并调用派生类的实现方法，达到对于传入的不同派生类实现不同的方法。 派生类要继承基类，并将自己的类型传入到模板参数当中 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // 基类模板 template \u0026lt;typename Derived\u0026gt; class Shape { public: void draw() { std::cout \u0026lt;\u0026lt; \u0026#34;所有类型draw的公用代码\u0026#34; \u0026lt;\u0026lt; std::endl;\t//进行复用，不用每个类型都写一份 static_cast\u0026lt;Derived*\u0026gt;(this)-\u0026gt;drawImpl(); } }; // 派生类：Circle class Circle : public Shape\u0026lt;Circle\u0026gt; { public: void drawImpl() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a circle.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; // 派生类：Square class Square : public Shape\u0026lt;Square\u0026gt; { public: void drawImpl() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a square.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; int main() { Circle circle; Square square; circle.draw(); square.draw(); } 奇异模板特性 特点 动态多态实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Shape { public: void draw() { std::cout \u0026lt;\u0026lt; \u0026#34;所有类型draw的公用代码\u0026#34; \u0026lt;\u0026lt; std::endl; drawImp(); } virtual void drawImp() = 0; }; class Circle : public Shape { public: void drawImp() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a circle.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; class Square : public Shape { public: void drawImp() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a square.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; int main() { Shape* circle = new Circle; Shape* square = new Square; circle-\u0026gt;draw(); square-\u0026gt;draw(); } 奇异模板相较于动态多态，它在编译时确定，避免了运行时虚函数表的查询。\n不需要虚函数表，内存占用较小\n通过函数调用实现多态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 template \u0026lt;typename Derived\u0026gt; class Shape { public: void draw() { std::cout \u0026lt;\u0026lt; \u0026#34;shared draw\u0026#34; \u0026lt;\u0026lt; std::endl; static_cast\u0026lt;Derived*\u0026gt;(this)-\u0026gt;drawImpl(); } }; // 派生类：Circle class Circle : public Shape\u0026lt;Circle\u0026gt; { public: void drawImpl() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a circle.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; // 派生类：Square class Square : public Shape\u0026lt;Square\u0026gt; { public: void drawImpl() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a square.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; template\u0026lt;typename Shape\u0026gt; void Draw(Shape\u0026amp; shape) { shape.draw(); } int main() { Circle circle; Square square; Draw(circle); Draw(square); } 其对应的动态多态形式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Shape { public: void draw() { std::cout \u0026lt;\u0026lt; \u0026#34;所有类型draw的公用代码\u0026#34; \u0026lt;\u0026lt; std::endl; drawImp(); } virtual void drawImp() = 0; }; class Circle : public Shape { public: void drawImp() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a circle.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; class Square : public Shape { public: void drawImp() { std::cout \u0026lt;\u0026lt; \u0026#34;Drawing a square.\u0026#34; \u0026lt;\u0026lt; std::endl; } }; void Draw(Shape* shape) { shape-\u0026gt;draw(); } int main() { Shape* circle = new Circle; Shape* square = new Square; Draw(circle); Draw(square); } ","date":"2025-03-10T00:00:00Z","image":"https://zhichenf.github.io/p/curiously_recurring_templage_pattern/25_hu_2646d9c987fa84db.jpg","permalink":"https://zhichenf.github.io/p/curiously_recurring_templage_pattern/","title":"奇异模板"},{"content":"引言 Git 是一个分布式版本控制系统，用于在软件开发过程中跟踪源代码的变更。它由 Linus Torvalds 于 2005 年创建，最初是为了管理 Linux 内核的开发。本文主要介绍了git中的常见命令， 以及日常工作使用git的流程。\n参考资料: 【GeekHour】一小时Git教程_哔哩哔哩_bilibili\n基本操作 新建仓库 1 2 3 git init\t# 在本地创建一个空仓库 git clone 远程地址\t# 从远程服务器克隆仓库 新建仓库后生成.git文件，就是仓库 工作区域和文件状态 基本操作 新建文件都是未跟踪的文件，需要执行git add进行跟踪 1 2 3 4 5 6 git add 文件名 # 将文件放入暂存区（包括未跟踪的文件） git commit -m\u0026#34;解释\u0026#34; # 将文件放入本地仓库 git add -u 文件名 # 将已经跟踪的文件放入暂存区，对于新建的文件不能使用 git commit -am\u0026#34;解释\u0026#34; #相当于两个命令， git add -u , git commit -m\u0026#34;解释\u0026#34; 取消add 1 git restore --stage file2 # add过后将文件取消add,即从暂存区拿回工作区 git reset回退版本 1 2 3 4 5 6 7 8 git reset --soft HEAD^\t#本地仓库回退到上个版本，工作区和暂存区保留 git log # 查看当前仓库经历的版本 git log --oneline #一行表示 git reset --mix HEAD 版本号\t#本地仓库回退到指定版本，工作区保留，暂存区不保留 git reset --hard HEAD 版本号\t# 本地仓库回退到指定版本，工作区和暂存区不保留 注意事项\n回退版本会导致版本丢失，使用git reflog指令查看本地仓库的操作历史，存储再.git/log中 git reflog 的注意事项\n本地记录：git reflog 只记录本地仓库的操作历史，不会同步到远程仓库。 过期时间：默认情况下，git reflog 记录会保留 90 天。过期后，记录会被自动清理。 不能恢复未提交的更改：git reflog 只能恢复已经提交的记录，无法恢复未提交的更改（如工作目录中的修改）。 git diff查看差异 查看工作区，暂存区，本地仓库之间的差异 1 2 3 4 5 6 git diff # 查看工作区和暂存区文件的差异， #如果暂存区没有文件，查看工作区和本地仓库之间的差异，相当于git diff HEAD git diff HEAD # 查看工作区和本地仓库之间的差异 git diff --cached # 查看暂存区和本地仓库之间的差异 比较两个版本之间的差异 1 2 3 4 5 6 7 git diff 版本号1 版本号2 # 显示版本号2在版本号1基础上做了哪些修改 git diff HEAD~ HEAD\t# 显示最新版本在上一个版本基础上做了那些修改 # 上面的命令会显示所有文件发生的变化 # 查看指定文件的差异内容 git diff HEAD~ HEAD 文件名 比较两个分支之间的差异 git rm删除版本库中的文件 1 2 3 4 5 6 7 8 9 10 11 12 # 方法一 rm 文件名 # 先删除本地工作区文件 git add 文件名 # 更新暂存区 git commit -m\u0026#34;删除文件\u0026#34; # 同步到本地仓库 # 方法二 git rm 文件名 # 相当于方法一的1，2两步 git commit -m\u0026#34;删除文件\u0026#34; # 不删除工作区文件 git rm --cached 文件名 # 其作用是从暂存区删除，并取消跟踪，文件会变为未跟踪 # 后续可以执行git commit 删除本地仓库的文件，或者等下一次提交 .gitignore文件 添加忽略文件，文件名可以使用通配符\n系统或者软件自动生成的文件 编译长生的中间文件和结果文件 运行时生成日志文件、缓存文件、临时文件 涉及身份、密码、口令、密钥等敏感信息文件 对于已经在版本库中的文件，./gitignore文件不起作用，需要使用git rm \u0026ndash;cached命令取消跟踪文件，并告知暂存区删除文件\n远程仓库 配置ssh 进入用户的.ssh目录 使用ssh-keygen -t rsa生成ssh密钥(可以指定文件名和密码，一直回车就都不指定) 将公钥传到远程仓库 这样就可以使用ssh进行clone了\n如果指定了生成的文件名，那么就需要生成一个配置文件config，以github为例，文件名内容如下\n1 2 3 4 5 # github Host github.com HostName github.com PreferredAuthentications publickey IdentityFile ~/.ssh/指定的生成ssh密钥的文件名 与远程仓库关联 1 2 3 4 5 6 7 8 9 10 git remote add origin 远程仓库地址 # origin是远程仓库的别名，origin代指远程仓库 git branch -M main # 指定分支名称为main git push -u origin main\t# 将本地仓库和远程仓库关联起来 # 实际是 git push -u origin main:main 把本地仓库和远程仓库origin关联，把本地仓库main分支推送给远程仓库main分支 # 本地分支名称和远程分支名称相同，就可以吧main:main省略为一个main git remote -v # 显示关联远程仓库的信息 拉取远程仓库 1 2 git pull 远程仓库别名 远程仓库分支名:本地仓库分支名\t# 从远程仓库拉去分支，分支名相同只要写一个即可 # git pull 会自动执行一次合并操作，如果没有冲突，合并操作就会完成 关于冲突和git fetch，在分支冲突进行讲解 分支 分支操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 git branch # 查看分支 git branch 分支名 # 创建新分支 git switch 分支名 # 切换分支 # 上面两行命令可以用一个命令来表示 git swtich -c 分支名 #注意别忘了切换分支 git merge 将要合并的分支名 # 把将要合并的分支合并到当前分支当中 git log --graph --oneline # 查看分支图 # 如果分支合并后，该分支还存在，如果分支不再被需要，需要手动删除 git branch -d 分支名 # 删除已经合并的分支 git branch -D 分支名 # 强制删除分支，不管有没有合并 解决冲突 1 2 3 4 5 6 7 8 # git merge发生冲突如何解决 # git merge发生冲突后，文件名里的内容会把冲突前后的差别都展示出来 git status # 查看信息 git diff # 查看冲突的内容 vim 文件名 # 手动编辑文件 git commit -am\u0026#34;提交更改后的文件\u0026#34; # 提交并自动合并 git merge --abort # 终止合并 rebase操作 执行rebase会把分支变为一条直线\n找两分支的公共祖先\n1 2 3 git log --oneline --graph --decoreate --all\t# 查看详细分支信息 git rebase 分支1 # 把当前分支与分支1公共节点开始往后的当前分支全部放到分支1后面 会改变提交历史，要避免在共享分支上使用rebase git模型 gitFlow模型 Master和Develop为主分支，长期存在；其他分支为辅助分支，合并完要删除，具体流程如下图所示 githubFlow模型 自己创建分支修改后发出合并请求 ","date":"2025-03-09T00:00:00Z","image":"https://zhichenf.github.io/p/git/22_hu_be9046c9930ecd9e.jpg","permalink":"https://zhichenf.github.io/p/git/","title":"git基础教学"},{"content":"引言 观察者模式（Observer Pattern）是一种行为设计模式，它定义了对象之间的一对多依赖关系，使得当一个对象的状态发生改变时，所有依赖于它的对象都会自动收到通知并更新。这种模式通常用于实现事件处理系统、发布-订阅模型等。\n观察者模式 试想这样一个场景，有一个超市，里面有很多员工，他们需要共同管理这个超市 当员工入职的时候，员工就会与超市产生联系，订阅超市信息 这个时候，一个员工提出超市售卖商品进行折扣售卖，被超市采纳了。此时超市的相关信息被更改了，这时候超市就要通知所有的员工，不再按照原来价格进行售卖，而是用折扣价进行售卖。 过了几天后，又有个员工提出了另一种营销策略，被超市采纳，超市也要通知所有员工新的售卖策略 上述的情景就用到了观察者模式，超市是被观察的对象，员工就是观察者，当被观察的对象信息发生改变，就要告诉所有的观察者信息改变，观察者也能根据信息改变进行自己的处理。\n源码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 //有一个抽象的观察者，观察某一个行为，并针对这一行为有一个抽象的处理 //具体的观察者继承继承抽象的观察者，同时实现自己的具体的处理 //对于被观察的行为，行为的发起者有一个观察者列表，发起行为时，调用所有观察者的具体处理 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; class AbstractObserver { public: virtual void DoProcess(int num) = 0; //通知观察者对象，需要进行重写 virtual void ChangeSubject(int data) = 0; virtual ~AbstractObserver() = default; }; class AbstractSubject { public: virtual ~AbstractSubject() = default; virtual void ChangeData(int) = 0; virtual void NotifyObservers() = 0; void AddObserver(AbstractObserver* observer) {observers.push_back(observer);} void RemoveObserver(const AbstractObserver* observer) { for (auto it = observers.begin(); it != observers.end(); ++it) { if (*it == observer) { observers.erase(it); } } } protected: vector\u0026lt;AbstractObserver*\u0026gt; observers; }; class Observer1 final : public AbstractObserver { public: explicit Observer1(AbstractSubject* subject) : subject_(subject) { subject_-\u0026gt;AddObserver(this); } void DoProcess(const int num) override {cout \u0026lt;\u0026lt; \u0026#34;Observer1 : \u0026#34; \u0026lt;\u0026lt; num \u0026lt;\u0026lt; endl;} void ChangeSubject(const int data) override {subject_-\u0026gt;ChangeData(data);} private: AbstractSubject* subject_; }; class Observer2 final : public AbstractObserver { public: explicit Observer2(AbstractSubject* subject) : subject_(subject) { subject_-\u0026gt;AddObserver(this); } void DoProcess(const int num) override {cout \u0026lt;\u0026lt; \u0026#34;Observer2 : \u0026#34; \u0026lt;\u0026lt; num \u0026lt;\u0026lt; endl;} void ChangeSubject(const int data) override {subject_-\u0026gt;ChangeData(data);} private: AbstractSubject* subject_; }; class Observer3 final : public AbstractObserver { public: explicit Observer3(AbstractSubject* subject) : subject_(subject) { subject_-\u0026gt;AddObserver(this); } void DoProcess(const int num) override {cout \u0026lt;\u0026lt; \u0026#34;Observer3 : \u0026#34; \u0026lt;\u0026lt; num \u0026lt;\u0026lt; endl;} void ChangeSubject(const int data) override {subject_-\u0026gt;ChangeData(data);} private: AbstractSubject* subject_; }; class Subject final : public AbstractSubject { public: explicit Subject(const int data = 0) : data_(data){} void ChangeData(const int data) override { data_ = data; NotifyObservers(); } void NotifyObservers() override { for (const auto \u0026amp; observer : observers) { observer -\u0026gt; DoProcess(data_); } } private: int data_; }; int main() { AbstractSubject* subject = new Subject(); AbstractObserver* observer1 = new Observer1(subject); AbstractObserver* observer2 = new Observer2(subject); AbstractObserver* observer3 = new Observer3(subject); observer1 -\u0026gt; ChangeSubject(2); //观察者1更改目标，所有观察者都应该显示更改后的目标 cout \u0026lt;\u0026lt; \u0026#34;-------------------------------\u0026#34; \u0026lt;\u0026lt; endl; observer2 -\u0026gt; ChangeSubject(612); //观察者2更改目标，所有观察者都应该显示更改后的目标 subject -\u0026gt; RemoveObserver(observer1); //删除1号观察者 cout \u0026lt;\u0026lt; \u0026#34;-------------------------------\u0026#34; \u0026lt;\u0026lt; endl; observer3 -\u0026gt; ChangeSubject(100); //观察者3更改目标，所有观察者都应该显示更改后的目标 delete subject; delete observer1; delete observer2; delete observer3; } 下面就来说明上面代码中每一个类的作用\nAbstractObserver：抽象观察者类，\n提供了修改被观察对象的纯虚函数 提供了对被观察对象信息变化后进行处理的纯虚函数 Observer1，2，3：具体观察者类， 继承了抽象观察者类\n具有抽象被观察对象的指针，构造函数可以传入不同的具体被观察对象 实现了基类的纯虚函数，实现更改被观察对象的信息，以及观察对象信息改变后自己要做的事情（DoProcess）。 AbstractSubject：抽象被观察对象类\n提供了观察者集合 提供了增加观察者和删除观察者的函数（已经实现） 提供了更改数据和通知观察者的纯虚函数 Subject：具体的被观察对象类，继承了抽象的被观察对象类\n重写更改数据的纯虚函数，并在调用通知函数 重新通知函数，调用每一个观察者的DoProcess函数，并将被修改的数据传入进去 使用的时候，用抽象被观察对象类型指针接受一个具体的被观察对象指针，新建具体观察者的时候，传入这个指针（将被观察对象和观察者联系起来）。当一个观察者调用ChangeSubject时，其被观察对象会调用ChangeData函数更改信息，ChangeData函数里面还会调用NotifyObservers函数，在NotifyObservers函数里面，所有观察者会调用DoProcess应对被观察对象的数据变化。\n","date":"2025-03-08T00:00:00Z","image":"https://zhichenf.github.io/p/observer_pattern/19_hu_3ed6dfbd8d9fe901.jpg","permalink":"https://zhichenf.github.io/p/observer_pattern/","title":"观察者模式"},{"content":"引言 模板方法模式和策略模式都是行为设计模式，它们的目标都是通过封装算法来提高代码的灵活性和可维护性。然而，它们的实现方式和适用场景有所不同。下面就会介绍这两种设计模式\n模板方法模式 模板方法的核心思想就是，将操作步骤固定(相当于给你了一个模板)，而实现这些步骤的方法是可以替换的。\n举个例子，班主任想要对班级同学的成绩进行统计并进行排序，学校规定使用了一种软件，这种软件需要人为完成一些步骤才能实现统计。 将成绩登记到电脑上 对数据进行排序 公布成绩（不要这样做！！！不要打击学生的自尊心） 我们必须严格按照学校的要求来做，但是我们可以通过不同的方法来完成学校的要求 对于登记到电脑上 这好像只能将成绩一个一个登记到电脑上，没有其他办法了。 对数据进行排序 我们可以使用冒泡排序 也可以使用快速排序 最后公布成绩 我们可以只公布分数而不公布姓名 我们也可以将分数和姓名一起公布（要被学生骂死了） 言归正传，这种情况下学校使用的软件相当于一个模板方法，但是有些步骤我们可以选择不同的操作方式。这就是模板方法的思想。\n代码展现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Base { public: Base() = default; virtual ~Base() = default; void DoWork() { Step1();Step2();Step3(); Step4(); //钩子操作，空操作，由子类选择是否可以扩展这个操作 } private: static void Step1() {std::cout \u0026lt;\u0026lt; \u0026#34;step1\u0026#34; \u0026lt;\u0026lt; std::endl;} static void Step3() {std::cout \u0026lt;\u0026lt; \u0026#34;step3\u0026#34; \u0026lt;\u0026lt; std::endl;} protected: virtual void Step2() = 0; virtual void Step4() {} }; //用户来决定使用哪些方法。 class Derived1 : public Base { ~Derived1() override = default; void Step2() override {std::cout \u0026lt;\u0026lt; \u0026#34;derived1\u0026#39;s step2\u0026#34; \u0026lt;\u0026lt; std::endl;} }; class Derived2 : public Base { ~Derived2() override = default; void Step2() override {std::cout \u0026lt;\u0026lt; \u0026#34;derived2\u0026#39;s step2\u0026#34; \u0026lt;\u0026lt; std::endl;} void Step4() override {std::cout \u0026lt;\u0026lt; \u0026#34;extend the hook operator\u0026#34; \u0026lt;\u0026lt; std::endl;} }; int main() { Base* p1 = new Derived1(); Base* p2 = new Derived2(); p1-\u0026gt;DoWork(); std::cout \u0026lt;\u0026lt; \u0026#34;------------------\u0026#34; \u0026lt;\u0026lt; std::endl; p2-\u0026gt;DoWork(); delete p1; delete p2; return 0; } 上面的模板函数给出了四个step，其中step2已经被模板方法写死了，你只能这么做（非虚函数），但是有些step方法没有实现（纯虚函数），给了你自由度选择自己的操作方式。 我们可以看到有一个step4钩子操作，这个构造操作在基类里面通常是个空操作，用来说明在这个地方你可以选择进行一些操作来进行优化，但是如果你优化也ok，这取决与你自己的意愿。 这样用户就可以定义派生类来继承基类，重写方法来决定一要用什么具体的步骤，你也可以重写上面step4操作进行操作上的一些扩展。 策略模式 策略模式提供了一种切换算法的思想，对于一个功能，我们可以切换里面的算法来实现不同的功能。对于策略模式，程序员可以先实现不同的策略，等到需要切换策略的时候 ，换一种策略即可。\n场景：电商平台的折扣策略 假设你在一家电商公司工作，负责实现一个购物车的结算功能。购物车需要支持多种折扣策略，例如：\n无折扣：原价结算。 固定折扣：比如满100减10。 百分比折扣：比如打8折。 会员专属折扣：根据会员等级提供不同的折扣。 产品经理可能会随时调整折扣策略，或者根据促销活动动态切换折扣方式。如果每次需求变更都修改结算逻辑，代码会变得难以维护。这时，策略模式就可以派上用场了！\n策略模式的核心思想 策略模式的核心是：\n将不同的算法（或行为）封装成独立的类，并使它们可以互相替换。 通过组合而不是继承来实现算法的动态切换。 用策略模式实现折扣功能 首先我们实现一个打折的接口，再商品进行结算的时候，直接调用这些接口 我们可以实现不同的打折方案，在不同的时候，切换打折方案 这就是策略模式的思想 代码展现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 // strategy class CalcStrategy { public: virtual ~CalcStrategy() = default; virtual int calc(int a, int b) = 0; }; //用户拓展 class AddStrategy final : public CalcStrategy { int calc(const int a, const int b) override { return a + b;} }; class SubStrategy final : public CalcStrategy { int calc(const int a, const int b) override { return a - b;} }; class MulStrategy final : public CalcStrategy { int calc(const int a, const int b) override { return a * b;} }; class DivStrategy final : public CalcStrategy { int calc(const int a, const int b) override { return a / b;} }; //factory class Factory { public: virtual ~Factory() = default; virtual CalcStrategy* NewStrategy() =0; }; //用户拓展工厂方法 class AddFactory final : public Factory { CalcStrategy* NewStrategy() override {return new AddStrategy;} }; class SubFactory final : public Factory { CalcStrategy* NewStrategy() override {return new SubStrategy;} }; class MulFactory final : public Factory { CalcStrategy* NewStrategy() override {return new MulStrategy;} }; class DivFactory final : public Factory { CalcStrategy* NewStrategy() override {return new DivStrategy;} }; //end of factory class Calculator { public: explicit Calculator(Factory* strategy_factory) {strategy_ = strategy_factory-\u0026gt;NewStrategy();} int doCalc(const int a, const int b) const { return strategy_-\u0026gt;calc(a, b);} ~Calculator() { delete strategy_;} private: //对于策略，采用的是组合的方式来进行，可以切换不通的策略 CalcStrategy* strategy_; }; //end of strategy int main() { Factory* add = new AddFactory(); Factory* sub = new SubFactory(); Factory* mul = new MulFactory(); Factory* div = new DivFactory(); const Calculator calculator1(add); std::cout \u0026lt;\u0026lt; calculator1.doCalc(1,1) \u0026lt;\u0026lt; std::endl;; const Calculator calculator2(sub); std::cout \u0026lt;\u0026lt; calculator2.doCalc(1,1) \u0026lt;\u0026lt; std::endl;; const Calculator calculator3(mul); std::cout \u0026lt;\u0026lt; calculator3.doCalc(1,1) \u0026lt;\u0026lt; std::endl;; const Calculator calculator4(div); std::cout \u0026lt;\u0026lt; calculator4.doCalc(1,1) \u0026lt;\u0026lt; std::endl;; delete factory1; delete factory2; delete factory3; delete factory4; } 可以看出，我们有一个Calculator类用来实现一个计算的功能。 Calculater 里面组合了一个Strategy类（可以认为功能和实现功能的策略是紧密联系的），我们可以传入不同的策略类来切换策略实现不同的功能 在本例中，我们可以通过工厂方法创建不同的策略，在创建Strategy对象的时候，传入不同的策略从而达到实现不同的功能的目的。 两种模式的对比 最后，我们再来看一下这两种模式的对比\n对比维度 模板方法模式 策略模式 定义 定义算法的框架，子类可以重写部分步骤，但不改变算法结构。 定义一系列算法，封装每个算法，使它们可以互换。 核心思想 将算法的通用部分放在父类中，具体实现延迟到子类。 将算法的选择与使用分离，客户端可以动态选择不同的策略。 实现方式 基于继承，父类定义算法框架，子类实现具体步骤。 基于组合，通过接口或抽象类定义策略，具体策略由不同类实现。 灵活性 较低，算法的结构在父类中固定，子类只能改变部分步骤。 较高，客户端可以动态切换策略，算法可以完全替换。 ","date":"2025-03-06T00:00:00Z","image":"https://zhichenf.github.io/p/template_method_and_strategy/24_hu_530faa936f8bae7f.jpg","permalink":"https://zhichenf.github.io/p/template_method_and_strategy/","title":"模板方法和策略模式"},{"content":"引言 单例设计模式（Singleton Design Pattern）是一种创建型设计模式，它确保一个类只有一个实例，并提供一个全局访问点来获取该实例。这种模式常用于需要控制资源访问、配置管理或共享资源等场景。\n本文主要介绍单例设计模式的两种实现方式及其特点。\n懒汉模式 懒汉模式创建单例对象，是在需要用到该对象的时候才会去创建单例对象\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Singleton { public: static Singleton* getInstance(){\t//需要用到的时候，调用getInstance创建单例对象 if (nullptr == m_pSingleton) { m_pSingleton = new Singleton(); } return m_pSingleton; } private: static Singleton* m_pSingleton; Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;create instance\u0026#34; \u0026lt;\u0026lt; std::endl;} ~Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;destroy instance\u0026#34; \u0026lt;\u0026lt; std::endl;} // 删除拷贝构造函数和赋值运算符，确保单例唯一性 Singleton(const Singleton\u0026amp;) = delete; Singleton\u0026amp; operator=(const Singleton\u0026amp;) = delete; }; Singleton* Singleton::m_pSingleton = nullptr; 懒汉模式在多线程环境下，存在线程不安全性，当多个线程都同时调用getInstance时，如果此时并没有创建单例对象出来，那么多个线程可能同时走到nullptr == m_pSingleton当中去，这时候多个线程同时new单例对象，导致了不安全行为。\n解决线程安全可以通过加锁来解决 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Singleton { public: static Singleton* getInstance() { std::lock_guard\u0026lt;std::mutex\u0026gt; lock(mutex); // 加锁 if (instance == nullptr) { instance = new Singleton(); } return instance; } // 删除拷贝构造函数和赋值运算符 Singleton(const Singleton\u0026amp;) = delete; Singleton\u0026amp; operator=(const Singleton\u0026amp;) = delete; private: Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;create instance\u0026#34; \u0026lt;\u0026lt; std::endl;} ~Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;destroy instance\u0026#34; \u0026lt;\u0026lt; std::endl;} static Singleton* instance; static std::mutex mutex; }; // 初始化静态成员变量 Singleton* Singleton::instance = nullptr; std::mutex Singleton::mutex; 下面还有一种双重检查的方式，这种方式看似更加麻烦，但是也有设计巧妙的地方，读者可以好好思考一下为什么。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Singleton { public: static Singleton* getInstance() { if (instance == nullptr) { // 第一次检查 std::lock_guard\u0026lt;std::mutex\u0026gt; lock(mutex); // 加锁 if (instance == nullptr) { // 第二次检查 instance = new Singleton(); } } return instance; } // 删除拷贝构造函数和赋值运算符 Singleton(const Singleton\u0026amp;) = delete; Singleton\u0026amp; operator=(const Singleton\u0026amp;) = delete; private: Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;create instance\u0026#34; \u0026lt;\u0026lt; std::endl;} ~Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;destroy instance\u0026#34; \u0026lt;\u0026lt; std::endl;} static Singleton* instance; static std::mutex mutex; }; // 初始化静态成员变量 Singleton* Singleton::instance = nullptr; std::mutex Singleton::mutex; 上面这种模式为什么要检查两次呢，这不是更加麻烦吗？其实里面也有巧妙的设计。\n如果按照只检查一次的代码，那么无论什么时候去调用getInstance()都会发生加锁动作。 但是双重检查版本只有在单例对象没有创建的时候才会发生加锁，当对象已经被创建之后，第一次检查nullptr == instance就会跳过加锁过程，直接返回对象。这样会大大减少加锁的开销。 2025-4-13更新\n上面那种模式还会涉及一些内存顺序的问题，instance = new Singleton(); 会被分为三个步骤，分配内存，构造，将地址传给instance。但是有时候指令重排会将三个步骤重排为分配内存，将地址传给instance，构造，这时候当两个线程按下面流程进行getInstance()就会发生问题。\n线程A 线程B if (instance == nullptr) std::lock_guard\u0026lt;std::mutex\u0026gt; lock(mutex); // 加锁 if (instance == nullptr) { // 第二次检查 分配内存 将地址给instance (instance不再是nullptr了) if (instance == nullptr) return instance 开始使用instance（？？？instance好像还没有构造） 构造instance 解决办法就是使用c++当中的原子操作(atomic的操作可以指定一些内存顺序，内存顺序和atomic的内容以后再更新，这里主要强调单例模式的实现)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include \u0026lt;atomic\u0026gt; class Singleton { public: static Singleton* getInstance() { if (instance.load() == nullptr) { std::lock_guard\u0026lt;std::mutex\u0026gt; lock(mutex); if (instance.load() == nullptr) { instance.store(new Singleton()); } } return tmp; } // 删除拷贝构造函数和赋值运算符 Singleton(const Singleton\u0026amp;) = delete; Singleton\u0026amp; operator=(const Singleton\u0026amp;) = delete; private: Singleton() { std::cout \u0026lt;\u0026lt; \u0026#34;create instance\u0026#34; \u0026lt;\u0026lt; std::endl; } ~Singleton() { std::cout \u0026lt;\u0026lt; \u0026#34;destroy instance\u0026#34; \u0026lt;\u0026lt; std::endl; } static std::atomic\u0026lt;Singleton*\u0026gt; instance; // 改为 atomic static std::mutex mutex; }; // 初始化 std::atomic\u0026lt;Singleton*\u0026gt; Singleton::instance(nullptr); std::mutex Singleton::mutex; 饿汉模式 在定义对象的时候，就会创建单例对象出来，这个对象会存在程序的整个作用域。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Singleton { public: static Singleton* getInstance(){ if (nullptr == m_pSingleton) { m_pSingleton = new Singleton(); } return m_pSingleton; } private: int data = 0; static Singleton* m_pSingleton; Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;create instance\u0026#34; \u0026lt;\u0026lt; std::endl;} ~Singleton() {std::cout \u0026lt;\u0026lt; \u0026#34;destroy instance\u0026#34; \u0026lt;\u0026lt; std::endl;} // 删除拷贝构造函数和赋值运算符，确保单例唯一性 Singleton(const Singleton\u0026amp;) = delete; Singleton\u0026amp; operator=(const Singleton\u0026amp;) = delete; }; Singleton* Singleton::m_pSingleton = getInstance(); 单例对象在程序启动后就进行了创建，多线程模式下，并不会出现线程安全问题，每个线程都是直接使用已经创建的单例对象。\n","date":"2025-03-01T00:00:00Z","image":"https://zhichenf.github.io/p/singleton_pattern/18_hu_cddcda8d6cc1026.jpg","permalink":"https://zhichenf.github.io/p/singleton_pattern/","title":"单例模式"},{"content":"引言 ​\t最近在实现vector的时候，遇到了一个问题：\n1 2 template\u0026lt;typename InputIt\u0026gt; T* insert(const T* pos, InputIt first, InputIt last) ​\t对于上面一个模板函数，如果按照上面那种写法，当调用v.insert(pos,1,2)；的时候，可能也会走进上面那个函数并将InputIt推导为int类型，而实际情况是想要往pos位置上插入1个2。这就会导致编译器不知道调用哪个函数。那么应当如何解决这一个问题呢？下面就要介绍这篇文章的主人公了。\nSFINAE SFINAE（Substitution Failure Is Not An Error，替换失败不是错误）是 C++ 模板元编程中的一个重要规则。它的核心思想是：在模板实例化过程中，如果某个模板参数替换失败（例如，类型不匹配或表达式不合法），编译器不会报错，而是会忽略这个模板特化，继续尝试其他可能的模板特化或重载。\nSFINAE 的核心思想 替换（Substitution）： 在模板实例化时，编译器会用实际的类型或值替换模板参数。 例如，对于 template\u0026lt;typename T\u0026gt; void foo(T t)，如果调用 foo(42)，编译器会用 int 替换 T。 替换失败（Substitution Failure）： 如果在替换过程中，某个表达式或类型不合法（例如，类型没有某个成员函数，或操作符不支持），替换就会失败。 不是错误（Not An Error）： 如果替换失败，编译器不会报错，而是会忽略这个模板特化，继续尝试其他可能的模板特化或重载。 SFINAE常用工具 std::enable_if 1 2 3 4 5 6 7 template\u0026lt;bool B, typename T = void\u0026gt; struct enable_if {}; template\u0026lt;typename T\u0026gt; struct enable_if\u0026lt;true, T\u0026gt; { using type = T; }; 如果条件 B 为 true，std::enable_if\u0026lt;B, T\u0026gt; 会定义嵌套类型 type，其值为 T。 如果条件 B 为 false，std::enable_if\u0026lt;B, T\u0026gt; 不会定义嵌套类型 type，从而导致替换失败（SFINAE）。 例如:\n1 2 3 4 template\u0026lt;typename T, typename std::enable_if\u0026lt;std::is_integral\u0026lt;T\u0026gt;::value, int\u0026gt;::type = 0\u0026gt; void foo(T t) { // 只有 T 是整数类型时，这个函数才会启用 } std::void_t std::void_t 的核心思想是利用模板的替换规则：\n如果传递给 std::void_t 的类型或表达式是合法的，那么 std::void_t 会生成 void 类型。 如果传递给 std::void_t 的类型或表达式是非法的（例如，某个类型不存在或某个操作不支持），那么模板替换会失败，触发 SFINAE，编译器会忽略这个模板特化，而不会报错。 std::declval std::declval 的定义如下：\n1 2 template\u0026lt;typename T\u0026gt; std::add_rvalue_reference_t\u0026lt;T\u0026gt; declval() noexcept; std::add_rvalue_reference_t\u0026lt;T\u0026gt; 是类型特征（type trait），用于将类型 T 转换为右值引用 T\u0026amp;\u0026amp;。 noexcept 表示该函数不会抛出异常。 std::declval 的主要作用是在编译时生成一个类型的右值引用，而不需要实际构造该类型的对象。这在以下场景中非常有用：\n类型推导：在模板元编程中，推导某个表达式的类型。 SFINAE：检查某个类型是否支持特定操作（例如成员函数、操作符等）。 编译时表达式检查：在不实际构造对象的情况下，检查某个表达式是否合法。 例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 template\u0026lt;typename T, typename = void\u0026gt; struct has_foo : std::false_type {}; template\u0026lt;typename T\u0026gt;\t//使用declval检查T类型能否调用foo() struct has_foo\u0026lt;T, std::void_t\u0026lt;decltype(std::declval\u0026lt;T\u0026gt;().foo())\u0026gt;\u0026gt; : std::true_type {}; struct Bar { void foo() {} }; struct Baz {}; static_assert(has_foo\u0026lt;Bar\u0026gt;::value, \u0026#34;Bar should have foo()\u0026#34;); static_assert(!has_foo\u0026lt;Baz\u0026gt;::value, \u0026#34;Baz should not have foo()\u0026#34;); 最终实现insert 1 2 3 4 5 template\u0026lt; class InputIt, typename = std::void_t\u0026lt; decltype(*std::declval\u0026lt;InputIt\u0026gt;()), decltype(++std::declval\u0026lt;InputIt\u0026amp;\u0026gt;()) \u0026gt;\u0026gt; T* insert(const T* pos, InputIt first, InputIt last) 通过SFINAE，只有支持*和++操作的InputIt才会走进这个函数，从而解决最初的那个问题。\n","date":"2025-02-26T00:00:00Z","image":"https://zhichenf.github.io/p/sfinae/14_hu_56168add6c0c4cbf.jpg","permalink":"https://zhichenf.github.io/p/sfinae/","title":"SFINAE"},{"content":"引言 在C++中，类型萃取（Type Traits）是一种在编译时获取和操作类型信息的机制。它通过模板元编程技术实现，允许开发者根据类型的特性进行条件编译或执行不同的代码路径。类型萃取类通常定义在\u0026lt;type_traits\u0026gt;头文件中。\n常见类型萃取类 std::is_* 系列 这些类型萃取类用于检查类型是否符合某种条件。\nstd::is_integral\u0026lt;T\u0026gt;：检查类型 T 是否为整数类型。 std::is_floating_point\u0026lt;T\u0026gt;：检查类型 T 是否为浮点类型。 std::is_pointer\u0026lt;T\u0026gt;：检查类型 T 是否为指针类型。 std::is_reference\u0026lt;T\u0026gt;：检查类型 T 是否为引用类型。 std::is_array\u0026lt;T\u0026gt;：检查类型 T 是否为数组类型。 std::is_function\u0026lt;T\u0026gt;：检查类型 T 是否为函数类型。 std::is_const\u0026lt;T\u0026gt;：检查类型 T 是否为常量类型。 std::is_volatile\u0026lt;T\u0026gt;：检查类型 T 是否为易失类型。 std::is_class\u0026lt;T\u0026gt;：检查类型 T 是否为类类型。 std::is_union\u0026lt;T\u0026gt;：检查类型 T 是否为联合体类型。 std::is_*_v 系列（C++17引入） 这些是 std::is_* 系列的变量模板版本，简化了使用方式。\nstd::is_integral_v\u0026lt;T\u0026gt;：如果 T 是整数类型，返回 true，否则返回 false。 std::is_floating_point_v\u0026lt;T\u0026gt;：如果 T 是浮点类型，返回 true，否则返回 false。 std::is_same\u0026lt;T, U\u0026gt; std::is_same\u0026lt;T, U\u0026gt;：检查类型 T 和 U 是否相同。 std::is_same_v\u0026lt;T, U\u0026gt;：这是 std::is_same\u0026lt;T, U\u0026gt; 的变量模板版本。 std::remove_* 系列 这些类型萃取类用于修改类型（如去除常量、指针等）。\nstd::remove_const\u0026lt;T\u0026gt;：去除类型 T 的常量修饰符。 std::remove_volatile\u0026lt;T\u0026gt;：去除类型 T 的易失修饰符。 std::remove_cv\u0026lt;T\u0026gt;：去除类型 T 的常量和易失修饰符。 std::remove_reference\u0026lt;T\u0026gt;：去除类型 T 的引用。 std::remove_pointer\u0026lt;T\u0026gt;：去除类型 T 的指针。 std::remove_extent\u0026lt;T\u0026gt;：去除类型 T 的数组维度。 std::is_base_of\u0026lt;Base, Derived\u0026gt; 和 std::is_convertible\u0026lt;From, To\u0026gt; std::is_base_of\u0026lt;Base, Derived\u0026gt;：检查 Derived 是否是 Base 的派生类。 std::is_convertible\u0026lt;From, To\u0026gt;：检查类型 From 是否能转换为类型 To。 std::enable_if\u0026lt;T, U\u0026gt; 和 std::disable_if\u0026lt;T, U\u0026gt; std::enable_if\u0026lt;T, U\u0026gt;：当 T 为真时，启用类型 U。 std::disable_if\u0026lt;T, U\u0026gt;：当 T 为真时，禁用类型 U。 std::is_trivially_* 系列（C++11引入） 这些类型萃取类用于检查类型是否具有某些简单的特性，如无构造函数、无拷贝构造函数等。\nstd::is_trivially_copyable\u0026lt;T\u0026gt;：检查类型 T 是否是一个 trivially copyable 类型（可以直接内存拷贝的类型）。 std::is_trivially_constructible\u0026lt;T\u0026gt;：检查类型 T 是否可以通过简单的构造函数构造。 std::is_trivially_destructible\u0026lt;T\u0026gt;：检查类型 T 是否有简单的析构函数。 std::conditional\u0026lt;T, U, V\u0026gt;（C++11引入） 这个类型萃取类根据条件 T 选择类型 U 或 V。\nstd::conditional\u0026lt;T, U, V\u0026gt;：如果 T 为真，则类型为 U，否则为 V。 is_integral\u0026lt;T\u0026gt;的实现 使用模板特化实现简单的类型萃取 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template\u0026lt;typename T\u0026gt; class my_is_integral { public: static constexpr bool value = false; }; //特化版本 template\u0026lt;\u0026gt; class my_is_integral\u0026lt;int\u0026gt; : public std::true_type {}; // 下面这些类型也是integral类型,后面的讨论不再全部写出来 template\u0026lt;\u0026gt; class my_is_integral\u0026lt;bool\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;char\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;signed char\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;unsigned char\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;short\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;unsigned short\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;unsigned int\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;long\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;unsigned long\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;long long\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;unsigned long long\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;wchar_t\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;char16_t\u0026gt; : public std::true_type {}; template\u0026lt;\u0026gt; class my_is_integral\u0026lt;char32_t\u0026gt; : public std::true_type {}; 基于c++14的变量的模板 1 2 3 4 5 6 7 8 9 10 11 12 template\u0026lt;typename T\u0026gt; class my_is_integral { public: static constexpr bool value = false; }; //特化版本 template\u0026lt;\u0026gt; class my_is_integral\u0026lt;int\u0026gt; : public std::true_type {}; //变量的模板，可以之间使用my_is_intergral_value\u0026lt;int\u0026gt;作为true or false template\u0026lt;typename T\u0026gt; constexpr bool my_is_integral_value = my_is_integral\u0026lt;T\u0026gt;::value; 解决修饰符问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template\u0026lt;typename T\u0026gt; class my_is_integral_helper { public: static constexpr bool value = false; }; //特化版本 template\u0026lt;\u0026gt; class my_is_integral_helper\u0026lt;int\u0026gt; : public std::true_type {}; //使用helper类 //my_is_integral继承了helper类并且使用remove_cv_t消除了cv限定符 template\u0026lt;typename T\u0026gt; class my_is_integral : public my_is_integral_helper\u0026lt;std::remove_cv_t\u0026lt;T\u0026gt;\u0026gt; {}; template\u0026lt;typename T\u0026gt; constexpr bool my_is_integral_value = my_is_integral\u0026lt;T\u0026gt;::value; 这样，就基本实现了自己的is_integral类了\n","date":"2025-02-18T00:00:00Z","image":"https://zhichenf.github.io/p/type_traits/15_hu_10240ca98b78cc0f.jpg","permalink":"https://zhichenf.github.io/p/type_traits/","title":"类型萃取"},{"content":"引言 reactor模型是一种事件驱动的设计模式，广泛应用于并发编程中，特别是在网络编程和高性能服务器设计中。它的基本思想是通过集中式的事件分发机制来处理输入/输出事件和相关的操作。\n前置知识：线程池\nreactor实现简要介绍 本案例实现的reactor模型用于服务器端，可以和用户端建立多个连接，每当用户有数据发送过来时，就需要通过线程池执行对应的任务，当任务结束以后，再由主线程将数据发送回用户 两个重要的类 EventLoop类 事件循环监听机制，主要会监听三个事件 连接建立 消息到达，通常会将处理消息的任务交给线程池去处理 消息处理完成，将处理好的消息发送回去 TcpConnection类 该类主要记录服务器这边的所有连接，并提供接受和发送数据 三个回调函数(通常由自己去实现，可以更改实现，去完成不同的业务) 连接建立 我们可以打印连接信息 消息达到 在监听消息达到，并且用户没有断开时执行 我们去接受消息，然后将消息和连接和线程池任务绑定，将任务放到线程池的任务队列中，让线程去执行任务 连接断开 在监听消息到达时执行，当发现消息到达并且结果为0时，说明用户断开，执行回调函数。 我们可以打印连接断开信息 线程池任务处理逻辑 (通常由自己去实现，可以更改实现，去完成不同的业务) 先根据msg进行对应的任务，然后将msg2交给主函数去发送(将发送能力打包成一个可调用对象，交给主函数) 三个回调函数和线程池任务处理逻辑都在main函数所在文件夹中进行定义 源码实现 InetAddress（ip地址和端口号对象） 封装了ip和端口号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #ifndef INET_ADDRESS_H_ #define INET_ADDRESS_H_ #include \u0026lt;arpa/inet.h\u0026gt; #include \u0026lt;string\u0026gt; class InetAddress{ public: InetAddress(const std::string\u0026amp; ip, unsigned short port); InetAddress(const struct sockaddr_in\u0026amp; addr); ~InetAddress() = default; auto Ip() const -\u0026gt; std::string; auto Port() const -\u0026gt; unsigned short; auto GetPtr() const -\u0026gt; const struct sockaddr_in*; private: struct sockaddr_in addr_; }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include \u0026#34;inet_address.h\u0026#34; #include \u0026lt;strings.h\u0026gt; InetAddress::InetAddress(const std::string\u0026amp; ip, unsigned short port) { ::bzero(\u0026amp;addr_,sizeof(struct sockaddr_in)); addr_.sin_family = AF_INET; addr_.sin_port = htons(port); addr_.sin_addr.s_addr = inet_addr(ip.c_str()); } InetAddress::InetAddress(const struct sockaddr_in\u0026amp; addr) : addr_(addr) { } std::string InetAddress::Ip() const { return std::string(inet_ntoa(addr_.sin_addr)); } unsigned short InetAddress::Port() const { return ntohs(addr_.sin_port); } const struct sockaddr_in* InetAddress::GetPtr() const { return \u0026amp;addr_; } Socket对象 封装了socket文件描述符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #ifndef SOCKET_H_ #define SOCKET_H_ class Socket { public: Socket(); explicit Socket(int fd); ~Socket(); auto Fd() const -\u0026gt; int; Socket(const Socket\u0026amp;) = delete; void operator=(const Socket\u0026amp;) = delete; private: int fd_; }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/socket.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026#34;socket.h\u0026#34; Socket::Socket() { fd_ = ::socket(AF_INET,SOCK_STREAM, 0); if (fd_ \u0026lt; 0) { perror(\u0026#34;socket\u0026#34;); return; } } Socket::Socket(int fd) : fd_(fd) {} Socket::~Socket() { close(fd_); } int Socket::Fd() const { return fd_; } SocketIO对象 (用于发送接受数据) 封装了通过socket进行收发数据的操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #ifndef SOCKET_IO_H_ #define SOCKET_IO_H_ class SocketIO { public: explicit SocketIO(int fd); ~SocketIO(); auto Readn(char *buf, int len) -\u0026gt; int; auto ReadLine(char *buf, int len) -\u0026gt; int; auto Writen(const char *buf, int len) -\u0026gt; int; private: int fd_; }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;errno.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/socket.h\u0026gt; #include \u0026#34;socket_io.h\u0026#34; SocketIO::SocketIO(int fd) : fd_(fd) {} SocketIO::~SocketIO() { close(fd_); } //len = 10000 1500/6 1000/1 int SocketIO::Readn(char *buf, int len) { int left = len; char *pstr = buf; int ret = 0; while(left \u0026gt; 0) { ret = read(fd_, pstr, left); if(-1 == ret \u0026amp;\u0026amp; errno == EINTR) { continue; } else if(-1 == ret) { perror(\u0026#34;read error -1\u0026#34;); return -1; } else if(0 == ret) { break; } else { pstr += ret; left -= ret; } } return len - left; } int SocketIO::ReadLine(char *buf, int len) { int left = len - 1; char *pstr = buf; int ret = 0, total = 0; while(left \u0026gt; 0) { //MSG_PEEK不会将缓冲区中的数据进行清空,只会进行拷贝操作 ret = recv(fd_, pstr, left, MSG_PEEK); if(-1 == ret \u0026amp;\u0026amp; errno == EINTR) { continue; } else if(-1 == ret) { perror(\u0026#34;readLine error -1\u0026#34;); return -1; } else if(0 == ret) { break; } else { for(int idx = 0; idx \u0026lt; ret; ++idx) { if(pstr[idx] == \u0026#39;\\n\u0026#39;) { int sz = idx + 1; Readn(pstr, sz); pstr += sz; *pstr = \u0026#39;\\0\u0026#39;;//C风格字符串以\u0026#39;\\0\u0026#39;结尾 return total + sz; } } Readn(pstr, ret);//从内核态拷贝到用户态 total += ret; pstr += ret; left -= ret; } } *pstr = \u0026#39;\\0\u0026#39;; return total - left; } int SocketIO::Writen(const char *buf, int len) { int left = len; const char *pstr = buf; int ret = 0; while(left \u0026gt; 0) { ret = write(fd_, pstr, left); if(-1 == ret \u0026amp;\u0026amp; errno == EINTR) { continue; } else if(-1 == ret) { perror(\u0026#34;writen error -1\u0026#34;); return -1; } else if(0 == ret) { break; } else { pstr += ret; left -= ret; } } return len - left; } Acceptor对象(用于建立tcp连接) 封装了通过socket建立tcp连接的过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #ifndef ACCEPTOR_H_ #define ACCEPTOR_H_ #include \u0026lt;string\u0026gt; #include \u0026#34;socket.h\u0026#34; #include \u0026#34;inet_address.h\u0026#34; class Acceptor{ public: Acceptor(const std::string\u0026amp; ip, unsigned short port); ~Acceptor() = default; auto Ready() -\u0026gt; void;\t//服务器端准备 auto Accept() -\u0026gt; int;\t//服务器端获得新连接 auto Fd() -\u0026gt; int; private: auto SetReuseAddr() -\u0026gt; void; auto SetReusePort() -\u0026gt; void; auto Bind() -\u0026gt; void; auto Listen() -\u0026gt; void; private: Socket sock_; //welcome socket InetAddress addr_; }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include \u0026lt;stdio.h\u0026gt; #include \u0026#34;acceptor.h\u0026#34; Acceptor::Acceptor(const std::string\u0026amp; ip, unsigned short port) : sock_(), addr_(ip,port) {} void Acceptor::Ready() { SetReuseAddr(); SetReusePort(); Bind(); Listen(); } void Acceptor::SetReuseAddr() { int on = 1; int ret = setsockopt(sock_.Fd(), SOL_SOCKET, SO_REUSEADDR, \u0026amp;on, sizeof(on)); if (ret) { perror(\u0026#34;setsockopt\u0026#34;); return; } } void Acceptor::SetReusePort() { int on = 1; int ret = setsockopt(sock_.Fd(), SOL_SOCKET, SO_REUSEPORT, \u0026amp;on, sizeof(on)); if (ret) { perror(\u0026#34;setsockopt\u0026#34;); return; } } void Acceptor::Bind() { int ret = ::bind(sock_.Fd(),(struct sockaddr*)addr_.GetPtr(), sizeof(struct sockaddr)); if (-1 == ret) { perror(\u0026#34;bind\u0026#34;); return; } } void Acceptor::Listen() { int ret = ::listen(sock_.Fd(),128); if (-1 == ret) { perror(\u0026#34;listen\u0026#34;); return; } } int Acceptor::Accept() { int connfd = ::accept(sock_.Fd(),nullptr,nullptr); if (-1 == connfd) { perror(\u0026#34;accept\u0026#34;); return -1; } return connfd; } int Acceptor::Fd() { return sock_.Fd(); } EventLoop类 实现了事件循环机制，通过io多路复用对各种事件进行了监听 存储了连接所有连接fd和封装的连接类，可以进行连接上的处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #ifndef EVENT_LOOP_H_ #define EVENT_LOOP_H_ #include \u0026lt;vector\u0026gt; #include \u0026lt;map\u0026gt; #include \u0026lt;memory\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;mutex\u0026gt; class Acceptor; class TcpConnection; using TcpConnectionPtr = std::shared_ptr\u0026lt;TcpConnection\u0026gt;; using TcpConnectionCallback = std::function\u0026lt;void(const TcpConnectionPtr \u0026amp;)\u0026gt;; class EventLoop { using TcpConnectionPtr = std::shared_ptr\u0026lt;TcpConnection\u0026gt;; using Functor = std::function\u0026lt;void()\u0026gt;; public: EventLoop(Acceptor \u0026amp;acceptor); ~EventLoop(); //循环与否 auto Loop() -\u0026gt; void; auto Unloop() -\u0026gt; void; private: //封装类epoll_wait函数 auto WaitEpollFd() -\u0026gt; void; //处理新的连接 auto HandleNewConnection() -\u0026gt; void; //处理老的连接上的消息 auto HandleMessage(int fd) -\u0026gt; void; //epfd的创建 auto CreateEpollFd() -\u0026gt; int; //监听文件描述符 auto AddEpollReadFd(int fd) -\u0026gt; void; //取消文件描述符的监听 auto DelEpollReadFd(int fd) -\u0026gt; void; public: auto SetNewConnectionCallback(TcpConnectionCallback\u0026amp;\u0026amp; cb) -\u0026gt; void; auto SetMessageCallback(TcpConnectionCallback\u0026amp;\u0026amp; cb) -\u0026gt; void; auto SetCloseCallback(TcpConnectionCallback\u0026amp;\u0026amp; cb) -\u0026gt; void; private: auto HandleRead() -\u0026gt; void; auto DoPendingFunctors() -\u0026gt; void; auto CreateEventFd() -\u0026gt; int; public: auto Wakeup() -\u0026gt; void; auto RunInLoop(std::function\u0026lt;void()\u0026gt;\u0026amp;\u0026amp; f) -\u0026gt; void; private: int epfd_;//epoll_create创建的文件描述符 std::vector\u0026lt;struct epoll_event\u0026gt; evtList_;//存放满足条件的文件描述符的数据结构 bool isLooping_;//标识循环是否在运行的标志 Acceptor\u0026amp; acceptor_;//因为需要调用其中的accept函数 std::map\u0026lt;int, TcpConnectionPtr\u0026gt; conns_;//存放的是文件描述符与连接的键值对 TcpConnectionCallback onNewConnectionCb_;//连接建立 TcpConnectionCallback onMessageCb_;//消息到达 TcpConnectionCallback onCloseCb_;//连接断开 int evtfd_; //用于通知有消息处理完成，并放入到pendings_中了 std::vector\u0026lt;Functor\u0026gt; pendings_; //消息处理完后 std::mutex mutex_; }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/epoll.h\u0026gt; #include \u0026lt;sys/eventfd.h\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026#34;event_loop.h\u0026#34; #include \u0026#34;acceptor.h\u0026#34; #include \u0026#34;tcp_connection.h\u0026#34; EventLoop::EventLoop(Acceptor \u0026amp;acceptor) : epfd_(CreateEpollFd()), evtList_(1024), isLooping_(false), acceptor_(acceptor), evtfd_(CreateEventFd()) { //将listenfd放在红黑树上进行监听（socket） int listenfd = acceptor_.Fd(); AddEpollReadFd(listenfd); AddEpollReadFd(evtfd_); } EventLoop::~EventLoop() { close(epfd_); close(evtfd_); } //循环与否 void EventLoop::Loop() { isLooping_ = true; while(isLooping_) { WaitEpollFd(); } } void EventLoop::Unloop() { isLooping_ = false; } //封装类epoll_wait函数 void EventLoop::WaitEpollFd() { int nready = 0; do { nready = epoll_wait(epfd_, \u0026amp;*evtList_.begin(), evtList_.size(), 3000); } while(-1 == nready \u0026amp;\u0026amp; errno == EINTR); if(-1 == nready) { std::cerr \u0026lt;\u0026lt; \u0026#34;-1 == nready\u0026#34; \u0026lt;\u0026lt; std::endl; return; } else if(0 == nready) { std::cout \u0026lt;\u0026lt; \u0026#34;\u0026gt;\u0026gt;epoll_wait timeout\u0026#34; \u0026lt;\u0026lt; std::endl; } else { //可以判断一下，文件描述符是不是已经达到了1024 //如果达到1024就需要进行扩容 if(nready == (int)evtList_.size()) { evtList_.reserve(2 * nready); } for(int idx = 0; idx \u0026lt; nready; ++idx) { int fd = evtList_[idx].data.fd; //查看文件描述符是不是listenfd if(fd == acceptor_.Fd()) { // 事件1，有新连接 if(evtList_[idx].events \u0026amp; EPOLLIN) { //处理新的连接 HandleNewConnection(); } } else if (fd == evtfd_) { //事件2，有连接的任务处理完成了 if(evtList_[idx].events \u0026amp; EPOLLIN) { HandleRead(); DoPendingFunctors(); //主线程中发送被线程池处理好的数据 } } else { //事件3，连接有数据到达 if(evtList_[idx].events \u0026amp; EPOLLIN) { //处理老的连接 HandleMessage(fd); } } } } } //处理新的连接 void EventLoop::HandleNewConnection() { int connfd = acceptor_.Accept(); if(connfd \u0026lt; 0) { perror(\u0026#34;handleNewConnection accept\u0026#34;); return; } AddEpollReadFd(connfd); //就表明三次握手已经建立成功了 /* TcpConnection con(connfd); */ TcpConnectionPtr con(new TcpConnection(connfd, this)); //将三个回调函数注册给TcpConnection con-\u0026gt;SetNewConnectionCallback(onNewConnectionCb_);//连接建立的注册 con-\u0026gt;SetMessageCallback(onMessageCb_);//消息到达的注册 con-\u0026gt;SetCloseCallback(onCloseCb_);//连接断开的注册 //以键值对的形式存起来 /* _conns.insert(std::make_pair(connfd, con)); */ conns_[connfd] = con; con-\u0026gt;HandleNewConnectionCallback(); } //处理老的连接上的消息 void EventLoop::HandleMessage(int fd) { auto it = conns_.find(fd); if(it != conns_.end()) { //连接是存在的，可以进行数据的收发 bool flag = it-\u0026gt;second-\u0026gt;IsClosed();//读的时候客户端是不是与服务器断开 if(flag) { //连接已经断开 it-\u0026gt;second-\u0026gt;HandleCloseCallback();//连接断开的事件的触发时机已经到达 DelEpollReadFd(fd);//将文件描述符从红黑树上摘除掉 conns_.erase(it);//同时将该链接从map中删除 } else { it-\u0026gt;second-\u0026gt;HandleMessageCallback();//消息到达事件的触发时机已经到达 } } else { //连接不存在，可以直接让程序退出来 std::cout \u0026lt;\u0026lt; \u0026#34;连接不存在\u0026#34; \u0026lt;\u0026lt; std::endl; return; } } //epfd的创建 int EventLoop::CreateEpollFd() { int fd = ::epoll_create(100); if(fd \u0026lt; 0) { perror(\u0026#34;epoll_create\u0026#34;); return fd; } return fd; } //监听文件描述符 void EventLoop::AddEpollReadFd(int fd) { struct epoll_event evt; evt.events = EPOLLIN; evt.data.fd = fd; int ret = ::epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, \u0026amp;evt); if(ret \u0026lt; 0) { perror(\u0026#34;epoll_ctl add\u0026#34;); return; } } //取消文件描述符的监听 void EventLoop::DelEpollReadFd(int fd) { struct epoll_event evt; evt.events = EPOLLIN; evt.data.fd = fd; int ret = ::epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, \u0026amp;evt); if(ret \u0026lt; 0) { perror(\u0026#34;epoll_ctl add\u0026#34;); return; } } void EventLoop::SetNewConnectionCallback(TcpConnectionCallback \u0026amp;\u0026amp;cb) { onNewConnectionCb_ = std::move(cb); } void EventLoop::SetMessageCallback(TcpConnectionCallback \u0026amp;\u0026amp;cb) { onMessageCb_ = std::move(cb); } void EventLoop::SetCloseCallback(TcpConnectionCallback \u0026amp;\u0026amp;cb) { onCloseCb_ = std::move(cb); } void EventLoop::HandleRead() { uint64_t two; ssize_t ret = read(evtfd_, \u0026amp;two, sizeof(uint64_t)); if(ret != sizeof(uint64_t)) { perror(\u0026#34;read\u0026#34;); return; } } void EventLoop::DoPendingFunctors() { //将处理好的数据发送回去，处理函数已经绑定了连接和数据，所以直接执行就好了 std::vector\u0026lt;Functor\u0026gt; tmp; { std::lock_guard\u0026lt;std::mutex\u0026gt; lc(std::mutex); tmp.swap(pendings_); } for (auto\u0026amp; cb : tmp) { cb(); } } int EventLoop::CreateEventFd() { int fd = eventfd(10, 0); if(fd \u0026lt; 0) { perror(\u0026#34;eventfd\u0026#34;); return fd; } return fd; } void EventLoop::Wakeup() { uint64_t one = 1; ssize_t ret = write(evtfd_, \u0026amp;one, sizeof(uint64_t)); if(ret != sizeof(uint64_t)) { perror(\u0026#34;write\u0026#34;); return; } } void EventLoop::RunInLoop(Functor\u0026amp;\u0026amp; cb) { { std::lock_guard\u0026lt;std::mutex\u0026gt; lc(mutex_); pendings_.push_back(std::move(cb)); //交给主线程去执行回调函数,将处理好的数据发送回去 } Wakeup(); } TcpConnection类 tcp连接类，负责连接相关的操作\n可以调用底层的sockio类进行数据的收发，同时main里面的回调函数最终是通过连接类来执行的。(只是执行的时机由EventLoop类控制，在EventLoop类中调用TcpConnection的方法执行回调函数)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #ifndef TCP_CONNECTION_H_ #define TCP_CONNECTION_H_ #include \u0026lt;memory\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026#34;socket.h\u0026#34; #include \u0026#34;socket_io.h\u0026#34; #include \u0026#34;inet_address.h\u0026#34; #include \u0026#34;event_loop.h\u0026#34; class TcpConnection; using TcpConnectionPtr = std::shared_ptr\u0026lt;TcpConnection\u0026gt;; using TcpConnectionCallback = std::function\u0026lt;void(const TcpConnectionPtr \u0026amp;)\u0026gt;; class TcpConnection : public std::enable_shared_from_this\u0026lt;TcpConnection\u0026gt; { public: explicit TcpConnection(int fd, EventLoop* loop); ~TcpConnection(); auto Send(const std::string \u0026amp;msg) -\u0026gt; void; auto Receive() -\u0026gt; std::string; auto IsClosed() const -\u0026gt; bool; //为了方便调试的函数 auto ToString() -\u0026gt; std::string; auto SetNewConnectionCallback(const TcpConnectionCallback\u0026amp; cb) -\u0026gt; void; auto SetMessageCallback(const TcpConnectionCallback\u0026amp; cb) -\u0026gt; void; auto SetCloseCallback(const TcpConnectionCallback\u0026amp; cb) -\u0026gt; void; auto HandleNewConnectionCallback() -\u0026gt; void; auto HandleMessageCallback() -\u0026gt; void; auto HandleCloseCallback() -\u0026gt; void; auto SendInLoop(const std::string\u0026amp; msg) -\u0026gt; void; private: //获取本端地址与对端地址 auto GetLocalAddr() -\u0026gt; InetAddress; auto GetPeerAddr() -\u0026gt; InetAddress; private: SocketIO sockIO_; EventLoop* loop_; //为了调试而加入的三个数据成员 Socket sock_; InetAddress localAddr_; InetAddress peerAddr_; TcpConnectionCallback onNewConnectionCb_;//连接建立 TcpConnectionCallback onMessageCb_;//消息到达 TcpConnectionCallback onCloseCb_;//连接断开 }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include \u0026lt;iostream\u0026gt; #include \u0026lt;sstream\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026#34;tcp_connection.h\u0026#34; TcpConnection::TcpConnection(int fd, EventLoop* loop) : sockIO_(fd), loop_(loop), sock_(fd), localAddr_(GetLocalAddr()), peerAddr_(GetPeerAddr()) {} TcpConnection::~TcpConnection() {} void TcpConnection::Send(const std::string \u0026amp;msg) { sockIO_.Writen(msg.c_str(), msg.size()); } std::string TcpConnection::Receive() { char buff[65535] = {0}; sockIO_.ReadLine(buff, sizeof(buff)); return std::string(buff); } //可以通过该函数判断读的数据是不是空的，也就是有没有断开 bool TcpConnection::IsClosed() const { char buf[10]= {0}; int ret = ::recv(sock_.Fd(), buf, sizeof(buf), MSG_PEEK); return (0 == ret); } std::string TcpConnection::ToString() { std::ostringstream oss; oss \u0026lt;\u0026lt; localAddr_.Ip() \u0026lt;\u0026lt; \u0026#34;:\u0026#34; \u0026lt;\u0026lt; localAddr_.Port() \u0026lt;\u0026lt; \u0026#34;----\u0026gt;\u0026#34; \u0026lt;\u0026lt; peerAddr_.Ip() \u0026lt;\u0026lt; \u0026#34;:\u0026#34; \u0026lt;\u0026lt; peerAddr_.Port(); return oss.str(); } void TcpConnection::SendInLoop(const std::string\u0026amp; msg) { auto f = std::bind(\u0026amp;TcpConnection::Send,this,msg); //Send函数绑定好TcpConnection对象和处理好的msg if (loop_) { //发送操作交给eventloop,放入pendings数组中，并调用wakeup，让eventloop的evtfd_可读 //这样事件循环就能检测到事件，将pendings里的操作发送回去 loop_-\u0026gt;RunInLoop(std::move(f)); } } //获取本端的网络地址信息 InetAddress TcpConnection::GetLocalAddr() { struct sockaddr_in addr; socklen_t len = sizeof(struct sockaddr ); //获取本端地址的函数getsockname int ret = getsockname(sock_.Fd(), (struct sockaddr *)\u0026amp;addr, \u0026amp;len); if(-1 == ret) { perror(\u0026#34;getsockname\u0026#34;); } return InetAddress(addr); } //获取对端的网络地址信息 InetAddress TcpConnection::GetPeerAddr() { struct sockaddr_in addr; socklen_t len = sizeof(struct sockaddr ); //获取对端地址的函数getpeername int ret = getpeername(sock_.Fd(), (struct sockaddr *)\u0026amp;addr, \u0026amp;len); if(-1 == ret) { perror(\u0026#34;getpeername\u0026#34;); } return InetAddress(addr); } void TcpConnection::SetNewConnectionCallback(const TcpConnectionCallback \u0026amp;cb) { onNewConnectionCb_ = cb; } void TcpConnection::SetMessageCallback(const TcpConnectionCallback \u0026amp;cb) { onMessageCb_ = cb; } void TcpConnection::SetCloseCallback(const TcpConnectionCallback \u0026amp;cb) { onCloseCb_ = cb; } //三个回调的执行 void TcpConnection::HandleNewConnectionCallback() { if(onNewConnectionCb_) { /* _onNewConnectionCb(shared_ptr\u0026lt;TcpConnection\u0026gt;(this)); */ onNewConnectionCb_(shared_from_this()); } else { std::cout \u0026lt;\u0026lt; \u0026#34;_onNewConnectionCb == nullptr\u0026#34; \u0026lt;\u0026lt; std::endl; } } void TcpConnection::HandleMessageCallback() { if(onMessageCb_) { onMessageCb_(shared_from_this()); } else { std::cout \u0026lt;\u0026lt; \u0026#34;_onMessageCb == nullptr\u0026#34; \u0026lt;\u0026lt; std::endl; } } void TcpConnection::HandleCloseCallback() { if(onCloseCb_) { onCloseCb_(shared_from_this()); } else { std::cout \u0026lt;\u0026lt; \u0026#34;_onCloseCb == nullptr\u0026#34; \u0026lt;\u0026lt; std::endl; } } TcpServer类(封装EventLoop和Acceptor类) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #ifndef TCP_SERVER_H_ #define TCP_SERVER_H_ #include \u0026lt;string\u0026gt; #include \u0026#34;acceptor.h\u0026#34; #include \u0026#34;event_loop.h\u0026#34; class TcpServer{ using Callback = std::function\u0026lt;void(const TcpConnectionPtr \u0026amp;)\u0026gt;; public: TcpServer(const std::string\u0026amp; ip, unsigned short port); ~TcpServer() = default; auto Start() -\u0026gt; void; auto Stop() -\u0026gt; void; auto SetAllCallback(Callback\u0026amp;\u0026amp; cb1, Callback\u0026amp;\u0026amp; cb2, Callback\u0026amp;\u0026amp; cb3) -\u0026gt; void; private: Acceptor acceptor_; EventLoop loop_; }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026#34;tcp_server.h\u0026#34; TcpServer::TcpServer(const std::string\u0026amp; ip, unsigned short port) : acceptor_(ip,port), loop_(acceptor_) {} void TcpServer::Start() { acceptor_.Ready(); //创建socket连接，等待连接 loop_.Loop(); //开启事件循环 } void TcpServer::Stop() { loop_.Unloop(); } //设置处理事件回调函数 void TcpServer::SetAllCallback(Callback\u0026amp;\u0026amp; cb1, Callback\u0026amp;\u0026amp; cb2, Callback\u0026amp;\u0026amp; cb3) { loop_.SetNewConnectionCallback(std::move(cb1)); loop_.SetMessageCallback(std::move(cb2)); loop_.SetCloseCallback(std::move(cb3)); } main函数文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include \u0026lt;iostream\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026#34;acceptor.h\u0026#34; #include \u0026#34;tcp_connection.h\u0026#34; #include \u0026#34;event_loop.h\u0026#34; #include \u0026#34;tcp_server.h\u0026#34; #include \u0026#34;thread_pool.h\u0026#34; class MyTask{ public: MyTask(const std::string\u0026amp; msg, TcpConnectionPtr con) : msg_(msg), con_(con){ std::cout \u0026lt;\u0026lt; \u0026#34;construct \u0026#34; \u0026lt;\u0026lt; msg \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; \u0026#34;construct2 \u0026#34; \u0026lt;\u0026lt; msg_ \u0026lt;\u0026lt; std::endl; } void process(){ //线程池中线程执行的业务处理逻辑 std::string msg2 = \u0026#34;\u0026#34;; std::cout \u0026lt;\u0026lt; \u0026#34;before process: \u0026#34; \u0026lt;\u0026lt; msg_ \u0026lt;\u0026lt; std::endl; for (auto c : msg_) { if (c \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;z\u0026#39;) { msg2 += (c - 32); } else { msg2 += c; } } msg2 += \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;after process: \u0026#34; \u0026lt;\u0026lt; msg2 \u0026lt;\u0026lt; std::endl; //处理完成后，发送交给主线程去处理 //先将信息msg2交给TcpConnection,将msg2和connnction的send函数进行绑定，形成一个具有发送msg2能力的可调用对象void() //再将这个可调用对象交给EventLoop，放入到pendings中(所有线程处理完数据后，形成的可调用对象集合) //这里还会去让EventLoop的向evtfd_写入数据，让evtfd_可读，这样下一个loop循环检测到就去执行集合里的所有可调用对象 //即在主函数中将数据发送出去了 con_-\u0026gt;SendInLoop(msg2); } private: std::string msg_; TcpConnectionPtr con_; }; //连接建立 void onNewConnection(const TcpConnectionPtr \u0026amp;con) { std::cout \u0026lt;\u0026lt; con-\u0026gt;ToString() \u0026lt;\u0026lt; \u0026#34; has connected!\u0026#34; \u0026lt;\u0026lt; std::endl; } //文件描述符可读(消息到达) void onMessage(const TcpConnectionPtr \u0026amp;con, ThreadPool\u0026amp; pool) { std::string msg = con-\u0026gt;Receive(); std::cout \u0026lt;\u0026lt; \u0026#34;\u0026gt;\u0026gt;recv client msg = \u0026#34; \u0026lt;\u0026lt; msg \u0026lt;\u0026lt; std::endl; //业务处理逻辑函数交给线程池取完成 std::shared_ptr\u0026lt;MyTask\u0026gt; task = std::make_shared\u0026lt;MyTask\u0026gt;(msg,con); pool.AddTask(std::bind(\u0026amp;MyTask::process,task)); } //连接断开 void onClose(const TcpConnectionPtr \u0026amp;con) { std::cout \u0026lt;\u0026lt; con-\u0026gt;ToString() \u0026lt;\u0026lt; \u0026#34; has closed!\u0026#34; \u0026lt;\u0026lt; std::endl; } void test() { TcpServer tcpServer(\u0026#34;127.0.0.1\u0026#34;, 8888); ThreadPool pool(4,10); pool.Start(); tcpServer.SetAllCallback(onNewConnection, std::bind(\u0026amp;onMessage,std::placeholders::_1,std::ref(pool)), onClose); tcpServer.Start(); } int main(int argc, char *argv[]) { test(); return 0; } 线程池相关类（之前博客有相关介绍） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #ifndef TASKQUEUE_H_ #define TASKQUEUE_H_ #include \u0026lt;queue\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;condition_variable\u0026gt; class TaskQueue{ //表示任务的类型 using ElemType = std::function\u0026lt;void()\u0026gt;; public: explicit TaskQueue(int que_size); ~TaskQueue() = default; auto Push(ElemType\u0026amp;\u0026amp; ptask) -\u0026gt; void; auto Pop() -\u0026gt; ElemType; [[nodiscard]] auto IsFull() const -\u0026gt; bool; [[nodiscard]] auto IsEmpty() const -\u0026gt; bool; auto WakeUp() -\u0026gt; void; private: size_t que_size_; std::queue\u0026lt;ElemType\u0026gt; que_;\t//用于存放任务，任务应当是一个void()的可调用对象 std::mutex mutex_; std::condition_variable not_full_; std::condition_variable not_empty_; bool flag_; //为了唤醒所有的工作线程，可以让while退出 }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include \u0026#34;task_queue.h\u0026#34; TaskQueue::TaskQueue(const int que_size) : que_size_(que_size), mutex_(), not_full_(), not_empty_(), flag_(true) { } void TaskQueue::Push(ElemType\u0026amp;\u0026amp; ptask) { std::unique_lock lock(mutex_); while(IsFull()) { //等待not_full_来唤醒 not_full_.wait(lock); } que_.push(std::move(ptask)); not_empty_.notify_one(); } TaskQueue::ElemType TaskQueue::Pop() { std::unique_lock lock(mutex_); while(IsEmpty() \u0026amp;\u0026amp; flag_) { //等待not_empty_来唤醒 not_empty_.wait(lock); } if (flag_) { ElemType task = que_.front(); que_.pop(); not_full_.notify_one(); return task; } else { return nullptr; } } bool TaskQueue::IsFull() const { return que_.size() == que_size_; } bool TaskQueue::IsEmpty() const { return que_.empty(); } void TaskQueue::WakeUp() { flag_ = false; not_empty_.notify_all(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #ifndef THREAD_POOL_H_ #define THREAD_POOL_H_ #include \u0026lt;memory\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026#34;task_queue.h\u0026#34; class ThreadPool { using Task = std::function\u0026lt;void()\u0026gt;; friend class WorkThread; public: ThreadPool(size_t thread_num, size_t que_size); ~ThreadPool() = default; auto Start() -\u0026gt; void; auto Stop() -\u0026gt; void; auto AddTask(Task\u0026amp;\u0026amp; task) -\u0026gt; void; private: auto GetTask() -\u0026gt; Task; auto DoTask() -\u0026gt; void; private: size_t thread_num_; size_t que_size_; std::vector\u0026lt;std::unique_ptr\u0026lt;std::thread\u0026gt;\u0026gt; threads_; TaskQueue task_que_; bool is_exit_; }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include \u0026lt;iostream\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;chrono\u0026gt; #include \u0026#34;thread_pool.h\u0026#34; ThreadPool::ThreadPool(const size_t thread_num, const size_t que_size) : thread_num_(thread_num), que_size_(que_size), task_que_(que_size_), is_exit_(false) { threads_.reserve(thread_num_); } void ThreadPool::Start() { for (size_t i = 0; i \u0026lt; thread_num_; i++) { threads_.push_back(std::make_unique\u0026lt;std::thread\u0026gt;(std::bind(\u0026amp;ThreadPool::DoTask,this))); } } void ThreadPool::Stop() { //确保任务队列里的任务可以执行完 while(!task_que_.IsEmpty()) { std::this_thread::sleep_for(std::chrono::seconds(1)); } std::this_thread::sleep_for(std::chrono::seconds(2)); is_exit_ = true; task_que_.WakeUp(); for (size_t i = 0; i \u0026lt; thread_num_; i++) { threads_[i]-\u0026gt;join(); } } void ThreadPool::AddTask(Task\u0026amp;\u0026amp; task) { if (task) { task_que_.Push(std::move(task)); } } ThreadPool::Task ThreadPool::GetTask() { return task_que_.Pop(); } void ThreadPool::DoTask() { while(!is_exit_) { if (Task task = GetTask()) { task(); } } } 使用教学 主要代码写在MyTask类和三个回调函数中\n核心业务代码可以在MyTask中完成\n三个回调函数可以用来打印对应信息或进行其他处理\n","date":"2025-01-28T00:00:00Z","image":"https://zhichenf.github.io/p/reactor%E6%A8%A1%E5%9E%8B/11_hu_8e8f2ea4a7df2f63.jpg","permalink":"https://zhichenf.github.io/p/reactor%E6%A8%A1%E5%9E%8B/","title":"reactor模型"},{"content":"引言 std::shared_future 是 C++ 标准库中的一个类，用于表示可以共享的异步计算结果。它与 std::future 类似，但允许多个线程共享同一个异步结果，而 std::future 只能被单个线程访问。\nstd::shared_future std::shared_future 的特点 共享性: 多个 std::shared_future 对象可以引用同一个异步计算结果。 多次访问: 可以多次调用 get() 方法获取结果，而 std::future 的 get() 只能调用一次。 线程安全: 多个线程可以同时访问同一个 std::shared_future 对象。 与 std::future 的区别 特性 std::future std::shared_future 共享性 只能由一个线程访问 可以由多个线程共享 get() 调用次数 只能调用一次 可以多次调用 拷贝语义 不可拷贝，只能移动 可以拷贝，也可以移动 获取std::shared_future的方式 1 2 3 4 5 6 7 8 9 10 //方式一 移动future对象 std::promive\u0026lt;int\u0026gt; p; std::future\u0026lt;int\u0026gt; f = p.get_future(); std::shared_future\u0026lt;int\u0026gt; sf(std::move(f)); //方式二 调用futured的share()方法 std::shared_future\u0026lt;int\u0026gt; sf = f.shared(); //方式三 通过future的右值对象直接进行转移 std::shared_future\u0026lt;int\u0026gt; sf(p.get_future()); shared_future的代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void func(std::shared_future\u0026lt;int\u0026gt; sf) { std::cout \u0026lt;\u0026lt; \u0026#34;sf.get() is \u0026#34; \u0026lt;\u0026lt; sf.get() \u0026lt;\u0026lt; std::endl; } int main() { std::promise\u0026lt;int\u0026gt; p; std::future\u0026lt;int\u0026gt; f = p.get_future(); std::shared_future\u0026lt;int\u0026gt; sf = f.share(); //shared_future允许多个线程同时访问future对象，如果是future，则只能有一个线程访问 std::thread t1(func,sf); std::thread t2(func,sf); std::this_thread::sleep_for(std::chrono::seconds(5)); p.set_value(1); t1.join(); t2.join(); } 不同的shared_future对象都绑定同一个异步任务，异步任务结束后，所有的shared_future对象都可以通过get()获取值。 如果shared_future对象状态是ready，那么再新创建shared_future对象，其状态也是ready，可以通过get()立刻获得值。 std::shared_future 的核心作用就是将 std::future 分发（共享）给多个线程，使得多个线程可以同时调用 get() 获取异步任务的结果。 ","date":"2025-01-24T00:00:00Z","image":"https://zhichenf.github.io/p/shared_future/13_hu_f81c8bb9c6ad54c0.jpg","permalink":"https://zhichenf.github.io/p/shared_future/","title":"C++异步编程工具 (二)"},{"content":"引言 std::async、std::packaged_task 和 std::promise 是C++11引入的三个用于异步编程的工具，它们都与 std::future 配合使用，用于在多线程环境中执行任务并获取结果。本篇文中的工具都在\u0026lt;future\u0026gt;头文件中声明。\nstd::future std::future 的核心功能 获取异步操作的结果： 通过 get() 方法获取异步操作的结果。 如果结果尚未准备好，get() 会阻塞当前线程，直到结果可用。 检查异步操作的状态： 通过 valid() 方法检查 std::future 是否关联了一个有效的共享状态。 通过 wait()、wait_for() 和 wait_until() 方法等待异步操作完成。 移动语义： std::future 只能移动（Move），不能复制。 std::future 的主要方法 方法 描述 get() 获取异步操作的结果。如果结果未准备好，会阻塞当前线程直到结果可用。 valid() 检查 std::future 是否关联了一个有效的共享状态。 wait() 阻塞当前线程，直到异步操作完成。 wait_for() 阻塞当前线程一段时间，等待异步操作完成。 wait_until() 阻塞当前线程直到某个时间点，等待异步操作完成。 share() 将 std::future 转换为 std::shared_future，允许多个线程共享结果。 get和wait方法 wait方法没有返回值，只是等待异步操作完成\nget方法等待异步操作完成并会返回具体future\u0026lt;T\u0026gt;中T类型的值\n调用wait方法后，还可以调用get方法；返过来则不行\n具体使用看后面小节与其他工具相互配合 std::async async是一个模板函数，下面是这个函数的使用方法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int func1() { std::cout \u0026lt;\u0026lt; \u0026#34; (hello world in func1) \u0026#34; \u0026lt;\u0026lt; std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); return 1024; } void func2() { std::cout \u0026lt;\u0026lt; \u0026#34; (hello world in func2) \u0026#34; \u0026lt;\u0026lt; std::endl; } int main() { //第一个参数的两种形式deferred async(可以省略) std::future\u0026lt;int\u0026gt; f1 = std::async(std::launch::deferred, func1); //如果第一个参数是deferrd，则func1不会立即执行，等到后面调用get()或wait()方法时才会执行。 //如果第一个参数是async，创建一个新线程立刻执行func1，并返回结果。(async可以省略) //如果第一个参数是 deferred | async ，那么就会由async的实现自行决定选择运行方式 func2(); std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout \u0026lt;\u0026lt; \u0026#34; Result: \u0026#34; \u0026lt;\u0026lt; f1.get() \u0026lt;\u0026lt; std::endl; //等待func1执行完毕，并获取结果 } std::package_task std::packaged_task 的核心功能 包装可调用对象： std::packaged_task 可以包装一个可调用对象（函数、lambda表达式、函数对象等）。 关联 std::future： 通过 get_future() 方法，可以获取一个与任务结果关联的 std::future 对象。 执行任务： 调用 operator() 或将其传递给线程执行时，任务会被执行，并将返回值或异常自动设置到 std::future 中。 std::packaged_task 的主要方法 方法 描述 operator() 执行包装的可调用对象，并将返回值或异常与 std::future 关联。 get_future() 获取与 std::packaged_task 关联的 std::future 对象。 reset() 重置 std::packaged_task，使其可以重新包装一个新的可调用对象。 具体使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //函数模板参数指定函数的参数和返回值 //传入的函数不需要严格匹配，但是函数参数应当可以进行隐式转换 //该对象含有operator(),是调用对象，可以传给std::function //该对象可以移动，但是不能拷贝 int print(int d) { cout \u0026lt;\u0026lt; d \u0026lt;\u0026lt; endl; return d; } int main() { std::packaged_task\u0026lt;int(double)\u0026gt; task(print);\t//传入函数参数可以发生隐式类型转换即可 std::future\u0026lt;int\u0026gt; fut = task.get_future(); //在main函数执行 task(3.14159265359); cout \u0026lt;\u0026lt; fut.get() \u0026lt;\u0026lt; endl;\t//执行完task才会得到结果 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int print(int d) { this_thread::sleep_for(chrono::seconds(5)); cout \u0026lt;\u0026lt; d \u0026lt;\u0026lt; endl; return d; } void func(packaged_task\u0026lt;int(double)\u0026gt;\u0026amp;\u0026amp; task, const double d) { task(d); } int main() { packaged_task\u0026lt;int(double)\u0026gt; task(print); future\u0026lt;int\u0026gt; fut = task.get_future(); //在新线程上面去执行task thread t1(func,std::move(task),3.1415926); cout \u0026lt;\u0026lt; fut.get() \u0026lt;\u0026lt; endl;\t//等待t1线程的task执行结束才会获得值 t1.join } std::async和std::packaged_task\u0026lt;\u0026gt;使用对比\nstd::async\n将任务(函数)交给它，由它决定立刻执行还是延时执行，并直接返回future对象用来获取返回值。 std::launch::async立刻执行函数，会创建新线程 std::launch::deferred不会立刻执行，future对象调用wait或get时候才执行，不会创建新线程 std::packaged_task\n直接绑定任务，通过get_future成员方法得到future对象，通过oprator()进行任务的调用。 手动控制调用的时机 对比下面三个程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int print(int d) { this_thread::sleep_for(chrono::seconds(5)); cout \u0026lt;\u0026lt; d \u0026lt;\u0026lt; endl; return d; } void func(future\u0026lt;int\u0026gt;\u0026amp; fut) { this_thread::sleep_for(chrono::seconds(10)); fut.wait(); } int main() { future\u0026lt;int\u0026gt; fut = async(launch::deferred, print, 10); thread t1(func,std::ref(fut)); cout \u0026lt;\u0026lt; fut.get() \u0026lt;\u0026lt; endl;\t//立刻执行get,但是t1线程之后会执行wait导致报错。 t1.join(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int print(int d) { this_thread::sleep_for(chrono::seconds(5)); cout \u0026lt;\u0026lt; d \u0026lt;\u0026lt; endl; return d; } void func(future\u0026lt;int\u0026gt;\u0026amp; fut) { this_thread::sleep_for(chrono::seconds(10)); fut.wait(); } int main() { future\u0026lt;int\u0026gt; fut = async(launch::deferred, print, 10); thread t1(func,std::ref(fut)); auto state = fut.wait_for(chrono::seconds(0)); while (state != future_status::ready) {\t//异步任务还没有结束 state = fut.wait_for(chrono::milliseconds(200));\t//继续等待 } cout \u0026lt;\u0026lt; fut.get() \u0026lt;\u0026lt; endl;\t//调用get时候，异步任务以及结束，这里只是获得结果 t1.join(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int print(int d) { this_thread::sleep_for(chrono::seconds(5)); cout \u0026lt;\u0026lt; d \u0026lt;\u0026lt; endl; return d; } void func(packaged_task\u0026lt;int(int)\u0026gt;\u0026amp;\u0026amp; task, const int d) { this_thread::sleep_for(chrono::seconds(10)); task(d); } int main() { packaged_task\u0026lt;int(int)\u0026gt; task(print); future\u0026lt;int\u0026gt; fut = task.get_future(); thread t(func,std::move(task),10); cout \u0026lt;\u0026lt; fut.get() \u0026lt;\u0026lt; endl; t.join(); } std::promise std::promise 的核心功能 用途：std::promise 用于手动设置一个值或异常，并将其与 std::future 关联。 特点： 需要显式调用 set_value() 或 set_exception() 来设置值或异常。 适用于需要手动控制结果设置的场景。 通常用于将结果从一个线程传递到另一个线程。 std::promise 不能复制，但它支持移动语义（Move Semantics） std::promise 的主要方法 方法 描述 set_value() 设置值，并将 std::future 标记为就绪。 set_exception() 设置异常，并将 std::future 标记为就绪。 get_future() 获取与 std::promise 关联的 std::future 对象。 std::promise 的使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;future\u0026gt; void task(std::promise\u0026lt;int\u0026gt; prom) { std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作 prom.set_value(42); // 设置值 } int main() { std::promise\u0026lt;int\u0026gt; prom; std::future\u0026lt;int\u0026gt; fut = prom.get_future(); // 获取与 promise 关联的 future std::thread t(task, std::move(prom)); // 启动线程，传递 promise std::cout \u0026lt;\u0026lt; \u0026#34;Waiting for the result...\u0026#34; \u0026lt;\u0026lt; std::endl; int result = fut.get(); // 阻塞，直到 promise 设置值 std::cout \u0026lt;\u0026lt; \u0026#34;Result: \u0026#34; \u0026lt;\u0026lt; result \u0026lt;\u0026lt; std::endl; t.join(); // 等待线程结束 return 0; } future保存异常 一下几种情况可以将异常保存到future中，当future.get()的时候会重新抛出异常 async执行操作的时候发生异常，会将异常保存到future中。 packaged_task执行任务函数的时候抛出异常，会将异常保存到future中。 promise对象调用set_exception()设置异常。 当async和packaged_task对象在future未就绪的时候被销毁，他们的析构函数就会将std::future_error存储为异步任务的状态，它的值（std::future_error::code()方法获得）是std::future_errc::broken_promise(枚举类型)。 ","date":"2025-01-23T00:00:00Z","image":"https://zhichenf.github.io/p/future_wait_for_event/9_hu_206bffd6eb7f6994.jpg","permalink":"https://zhichenf.github.io/p/future_wait_for_event/","title":"C++异步编程工具 (一)"},{"content":"引言 CMake 是一个跨平台的构建系统生成器，用于自动化构建、测试和打包软件项目的过程。CMake 通过一系列源代码文件和配置设置，生成平台特定的构建文件（例如 Makefile、Visual Studio 项目文件等）。\nCMake语法 依次使用以下命令生成项目(对于CmakeLists.txt) 1 2 cmake -B build # 创建build文件夹和相关文件 cmake --build build # 生成项目 语法简介\ncmake -P *.cmake命令来运行cmake文件\n1 2 3 4 5 6 7 8 9 10 11 12 # 单行打印 message(\u0026#34;hello\u0026#34;) message(hello) # 多行打印 message(\u0026#34;first line second line\u0026#34;) message([[first line second line]]) # 打印cmake版本号，使用CMAKE_VERSION变量 ${} message(${CMAKE_VERSION}) set设置变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 set(Var1 \u0026#34;hello\u0026#34;) set(Var2 world) # 打印单行hello world message(${Var1} ${Var2}) # 打印多行hello world message(\u0026#34;${Var1} ${Var2}\u0026#34;) message(\u0026#34;------------------------\u0026#34;) set(VarLists a1 a2) message(${VarLists}) #a1a2 # 打印环境变量 message($ENV{PATH}) # 创建cmake项目的环境变量 set(ENV{CXX} \u0026#34;g++\u0026#34;) message($ENV{CXX}) # 删除刚刚创建的环境变量 unset(ENV{CXX}) list\nlist(操作方式 操作对象 可能有操作返回结果的变量) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 list(APPEND port p1 p2 p3) message(${port}) # p1p2p3 list(LENGTH port len) message(${len}) # 3 (变量长度为3) list(FIND port p2 index) message(${index}) # 1 (p2在1的位置) list(REMOVE_ITEM port p1) message(${port}) # p2p3 (删除p1) list(INSERT port 1 p2.5) message(${port}) # p2p2.5p3 (在1位置插入p2.5) list(REVERSE port) message(${port}) # p3p2.5p2 (进行反转) list(SORT port) message(${port}) # p2p2.5p3 (进行排序) 流程控制\nif流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 set(VAR1 TRUE) set(VAR2 FALSE) if(VAR1) # 简单判断 message(TRUE) else() message(FALSE) endif() if(NOT VAR1) # NOT不能是not，大小写敏感 message(TRUE) else() message(FALSE) endif() if(VAR1 AND VAR2) # OR用法也是如此 message(TRUE) else() message(FALSE) endif() if(1 LESS 2) # 比较,其实还是字符串的比较 message(\u0026#34;1 less 2\u0026#34;) else() message(\u0026#34;error\u0026#34;) endif() if(2 GREATER 1) message(\u0026#34;2 greater 1\u0026#34;) endif() if(2 EQUAL \u0026#34;2\u0026#34;) message(\u0026#34;2 equal \\\u0026#34;2\\\u0026#34;\u0026#34;) endif() for流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 foreach(VAR RANGE 3) message(${VAR}) # 0 1 2 3 (跟一般编程语言区别很大) endforeach() set(MY_LISTS 1 2 3) foreach(VAR IN LISTS MY_LISTS ITEMS 4 f) message(${VAR}) # 1 2 3 4 f endforeach() message(\u0026#34;----------------------\u0026#34;) set(L1 one two three) set(L2 1 2 3 4) foreach(num IN ZIP_LISTS L1 L2) message(\u0026#34;word = ${num_0}, num = ${num_1}\u0026#34;) endforeach() # word = one, num = 1 # word = two, num = 2 # word = three, num = 3 # word = , num = 4 ​\n函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function(MyFunc FirstArg) # 函数名MyFunc message(\u0026#34;MyFunc Name: ${CMAKE_CURRENT_FUNCTION}\u0026#34;) message(\u0026#34;FIRST ${FirstArg}\u0026#34;) # 使用形参列表使用参数 set(FirstArg \u0026#34;New value\u0026#34;) message(\u0026#34;FirstArg again: ${FirstArg}\u0026#34;) message(\u0026#34;ARGV0 ${ARGV0}\u0026#34;) # 不使用形参列表使用参数 message(\u0026#34;ARGV1 ${ARGV1}\u0026#34;) message(\u0026#34;ARGV2 ${ARGV2}\u0026#34;) endfunction() set(FirstArg \u0026#34;first value\u0026#34;) MyFunc(${FirstArg} \u0026#34;value\u0026#34;) # 可以传入和新参列表不一样数目的实参 message(\u0026#34;FirstArg ${FirstArg}\u0026#34;) # 函数内部修改不会改变函数外面 # MyFunc Name: MyFunc # FIRST first value # FirstArg again: New value # ARGV0 first value # ARGV1 value # ARGV2 # FirstArg first value 宏\n1 2 3 4 5 6 7 8 9 10 11 12 13 macro(Test myVar) set(myVar \u0026#34;new value\u0026#34;) # 新建的变量，不是上面的参数 message(\u0026#34;argument: ${myVar}\u0026#34;) #${myVar}会变为传入的myVar endmacro() set(myVar \u0026#34;First value\u0026#34;) message(\u0026#34;myVar: ${myVar}\u0026#34;) Test(\u0026#34;value\u0026#34;) # 将宏代码复制到了这里 message(\u0026#34;myVar: ${myVar}\u0026#34;) # myVar: First value # argument: value # myVar: new value 简单的CMake项目 在这个简单的CMake项目中，有如下的项目结构 整个项目只有一个CMakeLists.txt，所有的源文件和头文件都放在项目目录中，bin中存放的是最终的可执行程序。 CMakeLists.txt中的代码如下 1 2 3 4 5 6 7 cmake_minimum_required(VERSION 3.29.0) # 确定CMake版本 project(planning) # 项目名 set(CMAKE_CXX_STANDARD 17) # 使用的语言和版本 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) # 设定可执行文件的路径 aux_source_directory(. SRC_LISTS) # 添加当前目录所有源文件路径到SRC_LIST变量中 add_executable(planning_main ${SRC_LISTS}) # 通过源文件创建目标可执行文件 分文件编写的的CMake项目 在这个份文件的CMake项目中，有如下的项目结构 整个项目只有一个CMakeLists.txt，src文件夹里面存放的是源文件，include文件夹中存放的是头文件，bin中存放的是最终的可执行程序。 CMakeLists.txt中的代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 cmake_minimum_required(VERSION 3.20.0) # 确定CMake版本 project(my_hello) # 项目名 set(CMAKE_CXX_STANDARD 17) # 使用的语言和版本 include_directories(${PROJECT_SOURCE_DIR}/include) # 指定头文件搜索路径 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) # 设定可执行文件的路径 aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC_LIST) # 添加src目录下所有源文件路径到SRC_LIST变量中 add_executable( # 通过源文件创建目标可执行文件 my_hello ${SRC_LIST} ) 共享库 在创建共享库的项目中，有如下目录结构 注意到bin中多出了libcommon.dll动态链接库文件。同时，main程序在项目目录下，其他.cpp文件存放在了src目录中，头文件存放在了include目录中。 CMakeLists.txt中的代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 cmake_minimum_required(VERSION 3.29.0) project(planning) set(CMAKE_CXX_STANDARD 17) # 设置生成的动态库存放路径 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) #linux下使用 CMAKE_LIBRARY_OUTPUT_DIRECTORY 变量 #静态库使用 CMAKE_ARCHIVE_OUTPUT_DIRECTORY 变量 aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC_LISTS) # 创建了名为common的动态库 add_library(common SHARED ${SRC_LISTS} ) # add_library(common STATIC ${SRC_LISTS}) 创建的是静态库 # 为动态库添加头文件目录 target_include_directories(common PUBLIC ${PROJECT_SOURCE_DIR}/include ) #生成可执行程序 add_executable(planning_main planning_main.cpp) # 为目标 planning_main 设置头文件目录 target_include_directories(planning_main PUBLIC ${PROJECT_SOURCE_DIR}/include ) # 为目标 planning_main 链接 common 库 target_link_libraries(planning_main PUBLIC common ) CmakeLists嵌套 项目有如下目录结构 项目文件夹下有一个CmakeLists文件；src文件夹里有一个CmakeLists文件，主程序和几个类的文件夹；同时src中每个类各自的文件夹都包含了CmakeLists，.cpp，.h三个文件 嵌套使用的时候上层CmakeLists文件的变量会传入到下层的CmakeLists文件当中。\n项目文件夹下的CmakeLists.txt代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 cmake_minimum_required(VERSION 3.29.0) project(planning VERSION 0.0.1 DESCRIPTION \u0026#34;a demo of cmake planning\u0026#34; LANGUAGES CXX ) set(CMAKE_CXX_STANDARD 17) # 一些后面要使用的变量 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) set(PROCESS_DIR ${CMAKE_SOURCE_DIR}/src/process) set(PNC_MAP_DIR ${CMAKE_SOURCE_DIR}/src/pnc_map) # 增加子目录 add_subdirectory(src) src文件夹下的CmakeLists.txt代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 project(planning_main) # 添加子目录 add_subdirectory(pnc_map) add_subdirectory(process) # 根据planning_main.cpp形成可执行文件 add_executable(${PROJECT_NAME} planning_main.cpp) # 生成可执行文件需要process.h target_include_directories(${PROJECT_NAME} PUBLIC ${PROCESS_DIR} ) # 链接库需要process库 target_link_libraries(${PROJECT_NAME} PUBLIC process ) 两个类中的CmakeLists代码分别如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 project(process) add_library( ${PROJECT_NAME} SHARED process.cpp ) # 生成库需要pnc_map.h\t注意，这是根据代码的关系决定，本例代码中，类process有pnc_maplei\u0026#39;x的成员变量 target_include_directories(${PROJECT_NAME} PUBLIC ${PNC_MAP_DIR} ) # 生成库需要pnc_map库 target_link_libraries(${PROJECT_NAME} PUBLIC pnc_map ) 1 2 3 4 5 6 project(pnc_map) add_library( ${PROJECT_NAME} SHARED pnc_map.cpp ) ","date":"2025-01-10T00:00:00Z","image":"https://zhichenf.github.io/p/simple_cmake/12_hu_fc72509877dd02a6.jpg","permalink":"https://zhichenf.github.io/p/simple_cmake/","title":"CMake创建c++项目简明教程"},{"content":"引言 在C++11之前，标准库并不提供线程相关的支持（如std::thread、std::mutex等），因此在C++11之前创建线程池需要依赖于操作系统的线程库或第三方库，如 POSIX线程（pthread） 或 Boost.Thread。而在c++11之后，我们可以使用c++的标准库来实现线程池，从而写出跨平台的线程池。\n线程池简介 线程池的定义 线程池是一种线程管理模式，用于复用一组固定数量的线程来执行任务，而不是为每个任务都单独创建和销毁线程。通过线程池，任务被放入任务队列中，由空闲的线程依次取出并执行，从而提升性能和资源利用率。\n为什么需要线程池？ 线程创建和销毁的开销：\n每次创建和销毁线程都需要占用系统资源。 在线程数量较多时，这些开销会变得显著。 系统资源限制：\n一个进程中可以创建的线程数量是有限的（由系统资源决定）。 如果频繁创建过多线程，可能会导致资源耗尽。 高效的任务调度：\n使用线程池可以让多个任务由有限的线程处理，避免因线程切换导致的性能下降。 空闲线程可以立即复用，减少等待时间。 控制并发量：\n限制线程的数量，防止系统过载。 线程池的工作原理 初始化线程池：\n创建一定数量的线程，并让它们处于等待（阻塞）状态。 任务提交：\n新任务被添加到任务队列中。 任务执行：\n空闲线程从任务队列中取出任务并执行。 执行完任务后，线程继续等待下一个任务。 线程池销毁：\n停止线程池，等待所有线程完成任务并退出。 线程池的主要组件 任务队列：\n存放待执行的任务，可以是FIFO队列或优先级队列。 每个任务通常是一个函数或可调用对象。 线程集合：\n一组预创建的工作线程，用于执行任务。 同步机制：\n使用锁（如std::mutex或pthread_mutex_t）保护任务队列，防止多线程竞争条件。 使用条件变量（如std::condition_variable或pthread_cond_t）来通知线程任务的到来。 线程池管理器：\n提供接口来提交任务、管理线程池大小、以及终止线程池。 线程池的优点 减少资源消耗：\n复用线程，避免频繁创建和销毁线程。 提高系统性能：\n避免过多线程竞争CPU资源。 降低线程切换的开销。 方便任务管理：\n可控制任务的执行顺序（如任务优先级）。 可根据负载动态调整线程池的大小。 提高可扩展性：\n在多核处理器上，线程池能够更好地利用多核资源，实现并行化处理。 线程池的使用场景 高并发服务器：\n如Web服务器、数据库服务器等，需要处理大量短时间的任务。 CPU密集型任务：\n通过固定数量的线程并行处理计算任务，提高CPU利用率。 I/O密集型任务：\n处理网络请求或文件I/O时，通过线程池避免阻塞。 定时任务：\n周期性任务调度，例如日志清理、数据备份等。 示例：线程池的任务调度流程 应用程序提交任务。 任务被放入任务队列。 线程池中一个空闲线程取出任务并执行。 执行完成后，线程返回线程池，等待下一任务。 基于c++11的线程池的实现 task_queue类的设计 任务队列使用生产者消费者模型，将任务当作物品，每当将任务放到任务队列的时候，就会通知线程拿取任务，并让线程执行任务。 task_queue的接口如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #ifndef TASKQUEUE_H_ #define TASKQUEUE_H_ #include \u0026lt;queue\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;condition_variable\u0026gt; class TaskQueue{ //表示任务的类型 using ElemType = std::function\u0026lt;void()\u0026gt;; public: explicit TaskQueue(int que_size); ~TaskQueue() = default; void Push(ElemType\u0026amp;\u0026amp; ptask); ElemType Pop(); bool IsFull() const; bool IsEmpty() const; void WakeUp(); private: size_t que_size_; std::queue\u0026lt;ElemType\u0026gt; que_;\t//用于存放任务，任务应当是一个void()的可调用对象 std::mutex mutex_; std::condition_variable not_full_; std::condition_variable not_empty_; bool flag_; //为了唤醒所有的工作线程，可以让while退出 }; #endif task_queue类的实现如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include \u0026#34;task_queue.h\u0026#34; TaskQueue::TaskQueue(const int que_size) : que_size_(que_size), mutex_(), not_full_(), not_empty_(), flag_(true) { } void TaskQueue::Push(ElemType\u0026amp;\u0026amp; ptask) { std::unique_lock lock(mutex_); while(IsFull()) { //等待not_full_来唤醒 not_full_.wait(lock); } que_.push(std::move(ptask)); not_empty_.notify_one(); } TaskQueue::ElemType TaskQueue::Pop() { std::unique_lock lock(mutex_); while(IsEmpty() \u0026amp;\u0026amp; flag_) { //等待not_empty_来唤醒 not_empty_.wait(lock); } if (flag_) { ElemType task = que_.front(); que_.pop(); not_full_.notify_one(); return task; } else { return nullptr; } } bool TaskQueue::IsFull() const { return que_.size() == que_size_; } bool TaskQueue::IsEmpty() const { return que_.empty(); } void TaskQueue::WakeUp() { flag_ = false; not_empty_.notify_all(); } thread_pool类的设计 thread_pool用于创建多个线程，并可以添加和获取任务，让线程去执行任务。其中有一个DoTask函数，当作线程函数，让线程执行任务。DoTask在执行任务前应当获取任务，如果没有任务，线程就会阻塞等待任务。当任务到来时，通过条件变量唤醒线程，并获取到任务task(一个可调用对象)，这时候就可以直接调用task()去执行任务。 thread_pool的接口如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #ifndef THREAD_POOL_H_ #define THREAD_POOL_H_ #include \u0026lt;memory\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026#34;task_queue.h\u0026#34; class ThreadPool { using Task = std::function\u0026lt;void()\u0026gt;; public: ThreadPool(size_t thread_num, size_t que_size); ~ThreadPool() = default; void Start(); void Stop(); void AddTask(Task\u0026amp;\u0026amp; task); private: Task GetTask(); void DoTask(); private: size_t thread_num_; size_t que_size_; std::vector\u0026lt;std::unique_ptr\u0026lt;std::thread\u0026gt;\u0026gt; threads_; TaskQueue task_que_; bool is_exit_; }; #endif thread_pool的实现如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include \u0026lt;iostream\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;chrono\u0026gt; #include \u0026#34;thread_pool.h\u0026#34; ThreadPool::ThreadPool(const size_t thread_num, const size_t que_size) : thread_num_(thread_num), que_size_(que_size), task_que_(que_size_), is_exit_(false) { threads_.reserve(thread_num_); } void ThreadPool::Start() { for (size_t i = 0; i \u0026lt; thread_num_; i++) { threads_.push_back(std::make_unique\u0026lt;std::thread\u0026gt;(std::bind(\u0026amp;ThreadPool::DoTask,this))); } } void ThreadPool::Stop() { //确保任务队列里的任务可以执行完 while(!task_que_.IsEmpty()) { std::this_thread::sleep_for(std::chrono::seconds(1)); } std::this_thread::sleep_for(std::chrono::seconds(2)); is_exit_ = true; task_que_.WakeUp(); for (size_t i = 0; i \u0026lt; thread_num_; i++) { threads_[i]-\u0026gt;join(); } } void ThreadPool::AddTask(Task\u0026amp;\u0026amp; task) { if (task) { task_que_.Push(std::move(task)); } } ThreadPool::Task ThreadPool::GetTask() { return task_que_.Pop(); } void ThreadPool::DoTask() { while(!is_exit_) { if (Task task = GetTask()) { task(); } } } 设计上的细节 在终止线程池之前，要确保任务队列中的任务被全部取出。 当线程池终止的时候，会将退出标志设置为true，并且会唤醒所有睡着的线程，并告知退出，此时线程将不再会进入循环。 使用线程池 定义MyTask类，使用process()成员函数作为任务的具体执行过程，任务所需要的变量可以用MyTask的成员变量来表示。注意任务应当是void()类型的可调用对象，所以thread_pool添加任务的时候，需要绑定一下this指针。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026#34;thread_pool.h\u0026#34; //自己实现任务逻辑交给线程池去执行 class MyTask { public: explicit MyTask(const int num) : num_(num) {} void process() const { std::cout \u0026lt;\u0026lt; num_ \u0026lt;\u0026lt; std::endl; } char num_; }; int main() { ThreadPool pool(4, 10); std::vector\u0026lt;MyTask\u0026gt; tasks; for (int i = 0; i \u0026lt; 40; i++) { tasks.emplace_back(i+\u0026#39;A\u0026#39;); } pool.Start(); for (int i = 0; i \u0026lt; 40; i++) { pool.AddTask(std::bind(\u0026amp;MyTask::process,\u0026amp;tasks[i])); } pool.Stop(); } ","date":"2025-01-06T00:00:00Z","image":"https://zhichenf.github.io/p/c-11_thread_pool/10_hu_d36c68a04df1ccc7.jpg","permalink":"https://zhichenf.github.io/p/c-11_thread_pool/","title":"C++11线程池"}]