《操作系统真象还原》第7章
“
1.中断分类
把中断按事件来源分类,来自CPU外部的中断就称为外部中断,来自CPU内部的中断称为内部中断。外部中断按是否导致宕机来划分,可分为可屏蔽中断和不可屏蔽中断两种,而内部中断按中断是否正常来划分,可分为软中断和异常。
CPU根据此中断向量号在中断向量表或中断描述符表中检索对应的中断处理程序并去执行。
- int 8位立即数,通过它进行系统调用
- int3。调试指令,其所触发的中断向量号是3
- into。中断溢出指令,它所触发的中断向量号是4
- bound。这是检查数组索引越界指令,它可以触发5号中断,用于检查数组的索引下标是否在上下边界之内。
- ud2。未定义指令,这会触发第6号中断。该指令表示指令无效,CPU无法识别。
2.中断描述符表
任务门、中断门、陷阱们、调用门。
中断门:在进入中断后,自动把中断关闭,避免中断嵌套。Linux就是利用中断门实现的系统调用,就是那个著名的int 0x80。
陷阱门主要用于调试,它允许CPU响应更高级别的中断,所以允许中断嵌套。而对任务门来说,这是执行一个新任务,任务都应该在开中断的情况下进行,否则就独占CPU资源,操作系统也会由多任务退化成单任务了。
3.编写中断处理程序
因为在kernel.S中要调用C程序,一定会使当前寄存器环境破坏,所以要保存当前所使用的寄存器环境。所以在程序中先把ds、es、fs和gs这4个16位(栈中占32位)段寄存器压栈。然后再通过pushad(push all double word register)指令压入8个通用寄存器。又把%1,即中断向量号压入栈中作为idt_table数组中某元素所指向的中断处理程序的参数。
intr_exit的实现是先用add esp,4跳过压入中断处理程序的参数:中断向量号。接下来再以寄存器入栈的相反顺序依次弹出栈恢复到寄存器。之前用pushad一次性压入8个通用寄存器,与之对应的将8个通用寄存器一次性出栈指令是popad(pop all double word register)。然后再将栈中gs、fs、es、ds的值依次出栈恢复到gs、fs、es、ds寄存器。然后跳过栈指针指向的error_code的位置,通过add指令把esp加4。
”
8259A和8253A就一扫而过吧,微机课已经学过了。
步骤:
感觉本章按书中的顺序一点点编写的话会显得很啰嗦,就直接给出各文件的完整代码吧。
不过还是有些步骤的:
1.编写中断处理程序
2.改进中断处理程序
3.设置时钟频率
1.编写中断处理程序
①kernel/kernel.S:
1 [bits 32]
2 %define ERROR_CODE nop
3 %define ZERO push 0
4
5 extern put_str ;声明外部函数
6
7 section .data
8 intr_str db "interrupt occur!",0xa,0
9 global intr_entry_table
10 intr_entry_table:
11
12 %macro VECTOR 2
13 section .text
14 intr%1entry: ;每个中断处理程序都要压入中断向量号,一个中断类型一个中断处理程序
15 %2
16 push intr_str
17 call put_str
18 add esp,4 ;跳过参数
19
20 ;如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
21 mov al,0x20 ;中断结束命令EOI
22 out 0xa0,al ;向从片发送
23 out 0x20,al ;向主片发送
24
25 add esp,4 ;跨过error_code
26 iret ;从中断返回
27
28 section .data
29 dd intr%1entry ;存储各个中断入口程序的地址,形成intr_entry_table数组
30 %endmacro
31
32 VECTOR 0x0 ,ZERO
33 VECTOR 0x1 ,ZERO
34 VECTOR 0x2 ,ZERO
35 VECTOR 0x3 ,ZERO
36 VECTOR 0x4 ,ZERO
37 VECTOR 0x5 ,ZERO
38 VECTOR 0x6 ,ZERO
39 VECTOR 0x7 ,ZERO
40 VECTOR 0x8 ,ERROR_CODE
41 VECTOR 0x9 ,ZERO
42 VECTOR 0xA ,ERROR_CODE
43 VECTOR 0xB ,ERROR_CODE
44 VECTOR 0xC ,ERROR_CODE
45 VECTOR 0xD ,ERROR_CODE
46 VECTOR 0xE ,ERROR_CODE
47 VECTOR 0xF ,ZERO
48 VECTOR 0x10 ,ZERO
49 VECTOR 0x11 ,ERROR_CODE
50 VECTOR 0x12 ,ZERO
51 VECTOR 0x13 ,ZERO
52 VECTOR 0x14 ,ZERO
53 VECTOR 0x15 ,ZERO
54 VECTOR 0x16 ,ZERO
55 VECTOR 0x17 ,ZERO
56 VECTOR 0x18 ,ZERO
57 VECTOR 0x19 ,ZERO
58 VECTOR 0x1A ,ZERO
59 VECTOR 0x1B ,ZERO
60 VECTOR 0x1C ,ZERO
61 VECTOR 0x1D ,ZERO
62 VECTOR 0x1E ,ERROR_CODE
63 VECTOR 0x1F ,ZERO
64 VECTOR 0x20 ,ZERO
②kernel/interrupt.c:
(来自3月13日,注意书中有误:第76行的括号与书中不同,后面第11章大家就会看到这个问题非常致命,出现问题后很难调试出来。)
1 #include "interrupt.h"
2 #include "stdint.h"
3 #include "global.h"
4 #include "io.h"
5
6
7 #define PIC_M_CTRL 0x20 // 主片的控制端口是0x20
8 #define PIC_M_DATA 0x21 // 主片的数据端口是0x21
9 #define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
10 #define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1
11
12 #define IDT_DESC_CNT 0x21 // 目前总共支持的中断数
13
14 /*中断门描述符结构体*/
15 struct gate_desc{
16 uint16_t func_offset_low_word;
17 uint16_t selector;
18 uint8_t dcount; // 此项为双字计数字段,是门描述符的第4字节,固定值
19 uint8_t attribute; // 属性值
20 uint16_t func_offset_high_word;
21 };
22
23 // 静态函数声明,非必须
24 static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function);
25 static struct gate_desc idt[IDT_DESC_CNT]; // IDT是中断描述符表,本质上就是个中断描述符数组
26
27 extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组
28
29 /* 初始化可编程中断控制器8259A */
30 static void pic_init(void) {
31
32 /*初始化主片*/
33 outb(PIC_M_CTRL,0x11);
34 outb(PIC_M_DATA,0x20);
35 outb(PIC_M_DATA,0x04);
36 outb(PIC_M_DATA,0x01);
37
38 /*初始化从片*/
39 outb(PIC_S_CTRL,0x11);
40 outb(PIC_S_DATA,0x28);
41 outb(PIC_S_DATA,0x02);
42 outb(PIC_S_DATA,0x01);
43
44 /*打开主片上IR0,也就是目前只接受时钟产生的中断*/
45 outb(PIC_M_DATA,0xfe);
46 outb(PIC_S_DATA,0xff);
47
48 put_str(" pic_init done\n");
49 }
50
51 /*创建中断门描述符*/
52 static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function){
53 p_gdesc->func_offset_low_word=(uint32_t)function & 0x0000ffff;
54 p_gdesc->selector=SELECTOR_K_CODE; // 中断后进入内核代码
55 p_gdesc->dcount=0;
56 p_gdesc->attribute=attr;
57 p_gdesc->func_offset_high_word=((uint32_t)function & 0xffff0000)>>16;
58 }
59
60 /*初始化中断描述符表*/
61 static void idt_desc_init(void){
62 int i;
63 for (i=0;i<IDT_DESC_CNT;++i){
64 make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
65 }
66 put_str(" idt_desc_init done\n");
67 }
68
69 /*完成有关中断的所有初始化工作*/
70 void idt_init(){
71 put_str("idt_init start\n");
72 idt_desc_init(); // 初始化中断描述符表
73 pic_init(); // 初始化PIC(8259A)
74
75 /*加载idt*/
76 uint64_t idt_operand=((sizeof(idt)-1)|((uint64_t)(uint32_t)idt<<16));
77 asm volatile("lidt %0"::"m"(idt_operand));
78 put_str("idt_init done\n");
79 }
③kernel/interrupt.h:
1 #ifndef __KERNEL_INTERRUPT_H
2 #define __KERNEL_INTERRUPT_H
3 #include "stdint.h"
4 typedef void* intr_handler;
5 void idt_init(void);
6 #endif
④kernel/global.h:
1 #ifndef __KERNEL_GLOBAL_H
2 #define __KERNEL_GLOBAL_H
3 #include "stdint.h"
4
5 #define RPL0 0
6 #define RPL1 1
7 #define RPL2 2
8 #define RPL3 3
9
10 #define TI_GDT 0
11 #define TI_LDT 1
12
13 #define SELECTOR_K_CODE ((1<<3)+(TI_GDT<<2)+RPL0)
14 #define SELECTOR_K_DATA ((2<<3)+(TI_GDT<<2)+RPL0)
15 #define SELECTOR_K_STACK SELECTOR_K_DATA
16 #define SELECTOR_K_GS ((3<<3)+(TI_GDT<<2)+RPL0)
17
18 /*-------------- IDT描述符属性 ------------------*/
19 #define IDT_DESC_P 1
20 #define IDT_DESC_DPL0 0
21 #define IDT_DESC_DPL3 3
22 #define IDT_DESC_32_TYPE 0xE // 32位的门
23 #define IDT_DESC_16_TYPE 0x6 // 16位的门,不会用到,定义它只为和32位门区分
24 #define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P<<7)+(IDT_DESC_DPL0<<5)+IDT_DESC_32_TYPE)
25 #define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P<<7)+(IDT_DESC_DPL3<<5)+IDT_DESC_32_TYPE)
26
27 #endif
⑤lib/kernel/io.h:
1 /***************** 机器模式 *******************
2 * b -- 输出寄存器QImode名称,即寄存器中最低8位:[a-d]l
3 * w -- 输出寄存器HImode名称,即寄存器中2字节的部分,如[a-d]x
4 *
5 * HImode
6 * "Half-Integer"模式,表示一个两字节的整数
7 * QImode
8 * "Quarter-Integer"模式,表示一个一字节的整数
9 * *********************************************************/
10
11 #ifndef __LIB_IO_H
12 #define __LIB_IO_H
13 #include "stdint.h"
14
15 /* 向端口port写入一个字节*/
16 static inline void outb(uint16_t port,uint8_t data){
17 /***************************************************
18 * 对端口指定N表示0~255,d表示dx存储端口号,
19 * %b0表示对应al,%w1表示对应dx */
20 asm volatile ("out %b0,%w1"::"a"(data),"Nd"(port));
21 /**************************************************/
22 }
23
24 /* 将addr处起始的word_cnt个字节写入端口port */
25 static inline void outsw(uint16_t port,const void* addr,uint32_t word_cnt){
26 /**************************************************
27 * +表示此限制既作输入,又作输出
28 * outsw是把ds:esi处的16位的内容写入port端口,我们在设置段描述符时,
29 * 已经将ds,es,ss段的选择子都设置为相同的值了,此时不用担心数据错乱。*/
30 asm volatile ("cld;rep outsw":"+S"(addr),"+c"(word_cnt):"d"(port));
31 /*************************************************/
32 }
33
34 /*将从端口port读入的一个字节返回*/
35 static inline uint8_t inb(uint16_t port){
36 uint8_t data;
37 asm volatile ("in %w1,%b0":"=a"(data):"Nd"(port));
38 return data;
39 }
40
41 /*将从端口port读入的word_cnt个字写入addr*/
42 static inline void insw(uint16_t port,void* addr,uint32_t word_cnt){
43 /*************************************************
44 * insw是将从端口port处读入的16位内容写入es:edi指向的内存,
45 * 我们在设置段描述符时,已经将ds,es,ss段的选择子都设置为相同的值了,
46 * 此时不用担心数据错乱。*/
47 asm volatile ("cld;rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
48 /************************************************/
49 }
50
51 #endif
⑥kernel/init.c:
1 #include "init.h"
2 #include "print.h"
3 #include "interrupt.h"
4
5 /*负责初始化所有模块*/
6 void init_all(){
7 put_str("init_all\n");
8 idt_init(); // 初始化中断
9 }
⑦kernel/init.h:
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
记得建立一个build目录,放一下编译链接命令(在bochs下):
1 gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
2 gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
3 gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
4 nasm -f elf -o build/print.o lib/kernel/print.S
5 nasm -f elf -o build/kernel.o kernel/kernel.S
6 ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o
7 dd if=/home/zbb/bochs/build/kernel.bin of=/home/zbb/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
8 bin/bochs -f bochsrc.disk
2.改进中断处理程序
涉及到修改两个文件,interrupt.c和kernel.S。
在interrupt.c的第27行加入:
在第81行加入:
1 /* 完成一般中断处理函数注册及异常名称注册 */
2 static void exception_init(void){
3 int i;
4 for (i=0;i<IDT_DESC_CNT;++i){
5 /* idt_table数组中的函数是进入中断后根据中断处理号调用的
6 * 见kernel/kernel.S的call [idt_table+1*4] */
7 idt_table[i]=general_intr_handler; // 默认为general_intr_handler
8 // 以后会由register_handler来注册具体处理函数
9 intr_name[i]="unknown"; // 先统一赋值为unknown
10 }
11 intr_name[0] = "#DE Divide Error";
12 intr_name[1] = "#DB Debug Exception";
13 intr_name[2] = "NMI Interrupt";
14 intr_name[3] = "#BP Breakpoint Exception";
15 intr_name[4] = "#OF Overflow Exception";
16 intr_name[5] = "#BR BOUND Range Exceeded Exception";
17 intr_name[6] = "#UD Invalid Opcode Exception";
18 intr_name[7] = "#NM Device Not Available Exception";
19 intr_name[8] = "#DF Double Fault Exception";
20 intr_name[9] = "Coprocessor Segment Overrun";
21 intr_name[10] = "#TS Invalid TSS Exception";
22 intr_name[11] = "#NP Segment Not Present";
23 intr_name[12] = "#SS Stack Fault Exception";
24 intr_name[13] = "#GP General Protection Exception";
25 intr_name[14] = "#PF Page-Fault Exception";
26 // intr_name[15] 第15项是intel保留项,未使用
27 intr_name[16] = "#MF x87 FPU Floating-Point Error";
28 intr_name[17] = "#AC Alignment Check Exception";
29 intr_name[18] = "#MC Machine-Check Exception";
30 intr_name[19] = "#XF SIMD Floating-Point Exception";
31 }
第117行加入:
对于kernel.S,直接给出完整代码:
1 [bits 32]
2 %define ERROR_CODE nop
3 %define ZERO push 0
4
5 extern put_str ;声明外部函数
6
7 section .data
8 ;intr_str db "interrupt occur!",0xa,0
9 global intr_entry_table
10 intr_entry_table:
11
12 %macro VECTOR 2
13 section .text
14 intr%1entry: ;每个中断处理程序都要压入中断向量号,一个中断类型一个中断处理程序
15 %2
16 ;以下是保存上下文环境
17 push ds
18 push es
19 push fs
20 push gs
21 pushad ;pushad压入32位寄存器,压栈顺序为eax,ecx,edx,ebx,esp,ebp,esi,edi
22
23 ;如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
24 mov al,0x20 ;中断结束命令EOI
25 out 0xa0,al ;向从片发送
26 out 0x20,al ;向主片发送
27
28 push %1 ;不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
29 call [idt_table+%1*4] ;调用idt_table中的C版本中断处理函数
30 jmp intr_exit
31
32 section .data
33 dd intr%1entry ;存储各个中断入口程序的地址,形成intr_entry_table数组
34 %endmacro
35
36 section .text
37 global intr_exit
38 intr_exit:
39 ;以下是恢复上下文环境
40 add esp,4 ;跳过中断号
41 popad
42 pop gs
43 pop fs
44 pop es
45 pop ds
46 add esp,4 ;跳过error_code
47 iretd
48
49 VECTOR 0x0 ,ZERO
50 VECTOR 0x1 ,ZERO
51 VECTOR 0x2 ,ZERO
52 VECTOR 0x3 ,ZERO
53 VECTOR 0x4 ,ZERO
54 VECTOR 0x5 ,ZERO
55 VECTOR 0x6 ,ZERO
56 VECTOR 0x7 ,ZERO
57 VECTOR 0x8 ,ERROR_CODE
58 VECTOR 0x9 ,ZERO
59 VECTOR 0xA ,ERROR_CODE
60 VECTOR 0xB ,ERROR_CODE
61 VECTOR 0xC ,ERROR_CODE
62 VECTOR 0xD ,ERROR_CODE
63 VECTOR 0xE ,ERROR_CODE
64 VECTOR 0xF ,ZERO
65 VECTOR 0x10 ,ZERO
66 VECTOR 0x11 ,ERROR_CODE
67 VECTOR 0x12 ,ZERO
68 VECTOR 0x13 ,ZERO
69 VECTOR 0x14 ,ZERO
70 VECTOR 0x15 ,ZERO
71 VECTOR 0x16 ,ZERO
72 VECTOR 0x17 ,ZERO
73 VECTOR 0x18 ,ZERO
74 VECTOR 0x19 ,ZERO
75 VECTOR 0x1A ,ZERO
76 VECTOR 0x1B ,ZERO
77 VECTOR 0x1C ,ZERO
78 VECTOR 0x1D ,ZERO
79 VECTOR 0x1E ,ERROR_CODE
80 VECTOR 0x1F ,ZERO
81 VECTOR 0x20 ,ZERO
编译运行还是那一套,结果如图:
不断打印“int vector:0x20”,时钟的中断向量号是0x20,效果还是符合预期的。
至于书中下一小节的调试实战,我就自己试试吧,不再演示了。
3.设置始终频率
再建一个device文件夹。
①device/timer.c:
1 #include "timer.h"
2 #include "io.h"
3 #include "print.h"
4
5 #define IRQ0_FREQUENCY 100
6 #define INPUT_FREQUENCY 1193180
7 #define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
8 #define COUNTER0_PORT 0X40
9 #define COUNTER0_NO 0
10 #define COUNTER_MODE 2
11 #define READ_WRITE_LATCH 3
12 #define PIT_COUNTROL_PORT 0x43
13
14 static void frequency_set(uint8_t counter_port,\
15 uint8_t counter_no,\
16 uint8_t rwl,\
17 uint8_t counter_mode,\
18 uint16_t counter_value)
19 {
20 outb(PIT_COUNTROL_PORT,(uint8_t) (counter_no<<6|rwl<<4|counter_mode<<1));
21 outb(counter_port,(uint8_t)counter_value);
22 outb(counter_port,(uint8_t)counter_value>>8);
23 }
24
25 void timer_init(void)
26 {
27 put_str("timer_init start!\n");
28 frequency_set(COUNTER0_PORT,\
29 COUNTER0_NO,\
30 READ_WRITE_LATCH,\
31 COUNTER_MODE,\
32 COUNTER0_VALUE);
33 put_str("timer_init done!\n");
34 }
②device/timer.h:
1 #ifndef __DEVICE_TIME_H
2 #define __DEVICE_TIME_H
3 #include "stdint.h"
4 void timer_init(void);
5 void mtime_sleep(uint32_t m_seconds);
6 #endif
③将kernel/init.c修改为:
1 #include "init.h"
2 #include "print.h"
3 #include "interrupt.h"
4 #include "../device/timer.h" //相对路径
5
6 /*负责初始化所有模块*/
7 void init_all(){
8 put_str("init_all\n");
9 idt_init(); // 初始化中断
10 timer_init(); // 初始化PIT
11 }
最终的编译链接命令:
1 gcc -m32 -I lib/kernel -c -o build/timer.o device/timer.c
2 gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
3 gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
4 gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
5 nasm -f elf -o build/print.o lib/kernel/print.S
6 nasm -f elf -o build/kernel.o kernel/kernel.S
7 ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o build/timer.o
8 dd if=/home/zbb/bochs/build/kernel.bin of=/home/zbb/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
9 bin/bochs -f bochsrc.disk
好长,已经感觉有点麻烦了 > <
如果你是这么多次实验一步步跟下来的,应该能看懂每条编译指令在干嘛,然后修改它,这也算是做这个项目get到的一个知识点吧。另外,我还感觉自己对于文件的相对/绝对位置关系,以及互相之间的调用方法有了更加深刻的理解。
在bochs中运行后,结果如图所示:
确实可以看到中断频率快了,快到IO显示都有些力不从心了,不是吗?
当我完成整个项目后,来梳理一下整个中断的过程:
1.首先是初始化中断——idt_init():
(1)初始化中断门描述符表gate_desc*,从而能够进入一般的中断函数和系统调用函数(当然,系统调用其实也是中断)
(2)一般中断的中断处理函数注册及异常名称注册,也就是准备第2步的中断处理函数:
①没有特别实现的中断处理函数将调用general_intr_handler()——在屏幕上显示中断信息并陷入死循环
②有特别实现的中断处理函数将调用register_handler()——将idt_table[vector_no]注册为相应的具体的中断处理程序
(3)初始化PIC(8259A,中断控制器)
(4)加载idt——lidt
2.对于kernel.S中的intr_entry_table:
(1)保存上下文环境——push各种寄存器、中断向量号
(2)调用中断处理函数——call [idt_table+偏移量]
期间栈的情况如下:
(3)退出中断——intr_exit,恢复上下文环境
3.对于kernel.S中的syscall_handler(以下部分本章未涉及,具体在第12章):
(1)保存上下文环境
(2)还要额外保存系统调用子功能的参数
(3)调用子功能处理函数——call [syscall_table+偏移量]
(4)将返回值存入当前内核栈中的eax,intr_exit恢复上下文
4.系统调用:
初始化:
除了第3步的初始化,还要进行以下步骤:
(1)在userprog/syscall-init.c的syscall_init()中注册syscall_table[系统调用号]=具体的函数(如sys_getpid())
(2)在lib/user/syscall.h中用枚举结构enum SYSCALL_NR存放系统调用子功能号,类似于宏定义
调用过程:
(1)在lib/user/syscall.c中,调用系统调用函数(如getpid())后会_syscall0/1/2(汇编实现)调用系统调用接口——压入参数(首个参数为系统调用号的宏)并"int 0x80"
(2)"int 0x80"通知CPU发生了中断,执行上面的第3步
第7章中断顺利结束,本书的内容也已接近一半了。“革命尚未成功,同志仍需努力”。(该去吃午饭了。。。)
参考博客:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库