18f458中断入口C语言_操作系统开发之——中断
这里先提交一个代码的错误,之前运行过快,没看出刷屏的问题:
// kernel/console.c...void init_console(void) {... // Before: // console_fixed_height = ScreenHeight - 16; // console_fixed_height = (ScreenHeight / 16 - 1) * 16;...}...void console_roll(void) {... // Before: // if (console_y > console_fixed_height) { // if (console_y >= console_fixed_height) {...}
Logo字符和信息是笔者自己加的
接下来,终于到了内核开发的核心部分:中断。
中断基本概念
中断,顾名思义就是中断当前任务并转去做其他事务:当你正在看一本书时,突然房间有个电话,你就先把书反扣在桌面上,然后去接电话,当你接完电话回来时,又把书扣回来继续看。
粗糙地用计算机术语描述就是:CPU在有序执行一段程序时,中断控制芯片突然传送了个信息请求CPU处理,这时CPU暂停当前执行的程序,然后将当前执行程序的各个寄存器的值和其他数据压入堆栈,转而去执行中断程序,然后回来将原来栈里的数据弹出,接着执行之前的程序。
没有中断,操作系统是没有灵魂的:你在键盘上敲一个键,显示一个图像,字符缓冲区输出一段文本,操作一个文件,鼠标的随便滑动,网络数据的传输,各种外设协同、驱动功能,进程调度等等都是建立在完善的中断系统之下完成的,相信读者此时已经了解到中断的重要性。失去了中断机制,操作系统就只是个单纯的“死循环”。
中断的分类
外部中断(硬件中断)
所谓外部中断,就是指CPU外部发生的中断,由硬件发起。常发生于输入输出设备、时钟,计时器,电源,网卡等部件和外设。外部中断有两根信号线:INTR(INTeRrupt)和 NMI(Non Maskable Interrupt)。INTR传来的中断比较无关紧要,CPU甚至可以不去处理,例如网卡和硬盘的中断请求,CPU可以晚一些执行,我们常称作可屏蔽中断;而NMI就很严重了,基本上都必须立马处理,比如内存读写出错,电源掉电等。
由于外设众多,执行中断程序的时候可能又会有另一个中断发生,因此中断程序一般都有个特点:执行要尽快,函数要可重入(常发生于多线程中的全局变量保护的问题)。
像这些可屏蔽中断,在Linux中,分为上半部分和下半部分:上半部分处理比较重要的,要快速完成的程序,下部分就是没那么重要的程序,一般在CPU空闲时或者合适的时期来处理,这里有个很生动的例子:
拿网卡举例子,网络中的数据通过网线到达网卡后,首先会被存储到网卡自己的缓冲区中,这个缓冲区容量不大(比起内存来说是非常小的),即使很大也有写满的那天,所以里面的数据必须立即被 CPU拿走,否则由于网卡缓冲区中无空余空间,后续到来的数据只能丢掉。鉴于这个刻不容缓的理由,网卡会立即发中断通知 CPU:“数据到了,赶紧取走”,这话说得无比坚定,丝毫没有商量的意思,CPU 立即放下手里的工作(其实并不是真地立即放下,怎么也得把当前正在执行的指令执行完,指令的执行必须是原子操作一气呵成,哪有执行一半指令的道理),马上执行网卡的中断处理程序,将网卡缓冲区中的数据拷贝到内核缓冲区中,至此,救火工作算是完成了,这就是所说的上半部。CPU 拿到网络数据后,处理数据的工作就不那么紧急了,它将在下半部中完成,这部分将在适当的时机被启动。
来源:《操作系统真象还原》
随着时代的发展,很多外设之间可以使用通道机制和DMA方式进行工作,大大得减轻了CPU的负担。之前的图形模式的Linear Frame Buffer就是其中一个例子。
内部中断(软件中断和异常)
中断源都是软件(可能有些人对软件的定义仍然是:Application。事实上,一系列按照特定顺序组织的计算机数据和指令的集合都是软件,你可以说操作系统是个很大的系统软件,也可以说BIOS是在ROM里躺着的软件,甚至一个dll、lib、so、数据、文档都可以叫做软件)发起的,常见于除数为0,运算溢出,指令的单步运行,程序运行至断点等等。至于其他,Intel官方以及列出个表了:
中断向量号 | 助记符 | 描述 | 起源 |
0 | #DE | 除 0 异常 | DIV和IDIV指令 |
1 | #DB | 调试异常 | 任何代码或数据引用 |
2 | / | NMI 中断 | 不可屏蔽的外部中断 |
3 | #BP | 断点异常 | INT 3指令 |
4 | #OF | 溢出 | INTO指令 |
5 | #BR | 对数组的引用超出边界 | BOUND指令 |
6 | #UD | 无效或未定义的操作码 | UD指令或保留的操作码 |
7 | #NM | 设备不可用(无数学协处理器) | 浮点或WAIT / FWAIT指令 |
8 | #DF | 双重故障(有错误代码) | 可以生成异常,NMI或INTR的任何指令 |
9 | #MF | 协处理器跨段操作 | 浮点指令 |
10 | #TS | 无效TSS(有错误代码) | 任务切换或TSS访问 |
11 | #NP | 段不存在(有错误代码) | 正在加载段寄存器或访问系统段 |
12 | #SS | 栈错误(有错误代码) | 堆栈操作和SS寄存器加载 |
13 | #GP | 常规保护(有错误代码) | 任何内存引用和其他保护检查 |
14 | #PF | 页故障(有错误代码) | 任何内存引用 |
15 | 保留 | ||
16 | #MF | 浮点处理单元错误 | 浮点或WAIT / FWAIT指令 |
17 | #AC | 对齐检查 | 存储器中的任何数据引用 |
18 | #MC | 机器检查 | 错误代码(如果有)和来源取决于型号 |
19 | #XM | SIMD(单指令多数据)浮点异常 | SIMD浮点指令 |
20 | #VE | 虚拟化 | EPT异常 |
21-31 | 保留 | ||
32-255 | 可屏蔽中断 | 来自INTR引脚或INT n指令的外部中断 |
翻译过来非常糟糕,读者可在intel白皮书的6.4.1看到原文和更多解释。对于表中的中断向量号,0~19号中断被CPU占用,20-31号中断被Intel保留,32~255号属于用户可自定义中断。不过我们一般都会中断按照习惯指定固定的设备。比如32号是timer中断,33号是键盘中断等等。下面我们开始介绍点实质性的东西。
中断描述符表
中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序入口的表,CPU接收到一个中断后,通过中断向量号在表中定位描述符,在该描述符中找到该中断处理程序的起始地址,接着执行该中断处理程序。中断描述符表里面可以包含以下任意一种门描述符:
任务门描述符
中断门描述符
陷阱门描述符
调用门描述符
他们的数据结构如下(每个门的上面是高32位,下面是低32位):
调用门的结构是这样的:
上面的英文我们见过很多次了,笔者就不再翻译了,我们只需要中断门描述符,其他门描述符读者感兴趣可以自行研究。我们直接用C语言表示该数据结构:
// include/interrupt.h#ifndef _INTERRUPT_H#define _INTERRUPT_H#include typedef union Type_S { struct { uint16_t Reserved:5; uint16_t SetZero:3; uint16_t P:1; uint16_t DPL:2; uint16_t Type_Flag:5; // |0 D 1 1 0| D: Size of gate: 1 = 32 bits; 0 = 16 bits } __attribute__((packed)); uint16_t All;} Type_S;typedef struct IDT_S { // High 32bits uint16_t Offset0_15; uint16_t Segment_Selector; // Low 32bits Type_S Type; uint16_t Offset16_31;} __attribute__((packed)) IDT_S;typedef struct IDTR_S { uint16_t Limite; uint32_t Base;} __attribute__((packed)) IDTR_S;#define IDT_BASE 0x00000000#define IDT_SIZE 0xFF#define INT_GATE 0x8E00 // 1000 1110 0000 0000#endif // _INTERRUPT_H
Type_S其实写成uint16_t就可以了,只不过笔者“严格”遵守门描述符的数据结构而已,__attribute__((packed))的是GNU专有的语法,gcc编译器在编译结构体部分的时候会自动根据结构体的数据结构添加一些数据类型进行内存对齐,使其运行效率提高,但是底层的东西不能这么做,是什么就是什么,packed就是为了告诉编译器不要“自作聪明”。接下来我们还要有一个IDT描述符的初始化函数:
// kernel/interrupt.c#include #include #include static void init_IDT_Descriptor(uint16_t Segment_Selector, uint32_t Offset, uint16_t Type, IDT_S *IDT);IDT_S IDT[256];IDTR_S IDTR;static void init_IDT_Descriptor(uint16_t Segment_Selector, uint32_t Offset, uint16_t Type, IDT_S *IDT) { IDT->Offset0_15 = Offset & 0xffff; IDT->Segment_Selector = Segment_Selector; IDT->Type.All = Type; IDT->Offset16_31 = (Offset & 0xffff0000) >> 16; return;}
说到string.h,我们需要自己实现,里面的模块以后会越用越多,全部实现的篇幅太长,这里只展示暂时用到的:
// libraries/string.c#include void* memcpy(void* dst, const void* src, uint8_t size) { char *d; const char *s; if (dst == NULL || src == NULL) return NULL; if ((char*)dst > ((char*)src + sizeof(src)) || ((char*)dst < (char*)src)) { d = (char*)dst; s = (char*)src; while (size--) *d++ = *s++; } else { d = ((char*)dst + size - 1); s = ((char*)src + size -1); while (size --) *d-- = *s--; } return dst;}void* memset(void* dst, uint32_t val, uint32_t size) { for (; 0 < size; size--) { *(char*)dst = val; dst++; } return dst;}uint32_t memcmp(void* buf1,void* buf2, uint32_t size) { while (size --> 0) { if (*(uint32_t*)buf1++ != *(uint32_t*)buf2++) { return 0; } } return 1;}void* memmove(void *dst, const void *src, size_t n) { char *tmp; const char *s; if (dst <= src) { tmp = dst; s = src; while (n--) *tmp++ = *s++; } else { tmp = dst; tmp += n; s = src; s += n; while (n--) *--tmp = *--s; } return dst;}
实现中断管理
接下来就是初始化整个IDT了,这部分内容比较多,但是都很简单。我们先捋一捋中断得整个流程,注意了,我们现在不考虑用户态和内核态(也就是特权级的问题):首先就是CPU(我们目前只讲单核处理器)接收到一个中断(向量号),这时保存现场,Intel官方是这么说的:
1. 将EFLAGS,CS和EIP寄存器的当前内容(按此顺序)压入堆栈。
2. 将错误代码(如果适用)压入堆栈。
3. 从中断门加载新代码段和新指令指针的段选择器(或陷阱门)分别进入CS和EIP寄存器。
4. 如果调用是通过中断门进行的,请清除EFLAGS寄存器中的IF标志。
5. 开始执行处理程序过程。
也就是说,我们要把当前所有寄存器和标准位保存起来,那么我们需要这个数据结构:
// include/interrupt.h...typedef struct Registers_S { uint32_t ds; uint32_t edi; uint32_t esi; uint32_t ebp; uint32_t esp; uint32_t ebx; uint32_t edx; uint32_t ecx; uint32_t eax; uint32_t Interrupt_Number; // 这里应该是Vector才对 uint32_t Error_Code; uint32_t eip; uint32_t cs; uint32_t eflags; uint32_t user_esp; uint32_t user_ss;} Registers_S;...
前面我们说过各个中断号的归属,我们需要为每个中断号实现一个中断服务程序,以及中断注册函数,那么接下来我们的头文件就是这样:
// include/interrupt.h...extern void ISR0(void);extern void ISR1(void);extern void ISR2(void);extern void ISR3(void);extern void ISR4(void);extern void ISR5(void);extern void ISR6(void);extern void ISR7(void);extern void ISR8(void);extern void ISR9(void);extern void ISR10(void);extern void ISR11(void);extern void ISR12(void);extern void ISR13(void);extern void ISR14(void);extern void ISR15(void);extern void ISR16(void);extern void ISR17(void);extern void ISR18(void);extern void ISR19(void);extern void ISR20(void);extern void ISR21(void);extern void ISR22(void);extern void ISR23(void);extern void ISR24(void);extern void ISR25(void);extern void ISR26(void);extern void ISR27(void);extern void ISR28(void);extern void ISR29(void);extern void ISR30(void);extern void ISR31(void);void init_IDT(void);...
但是具体的函数都是保存现场,实现起来都基本是一样的(有错误号和无错误号差一个指令),一个个写太费事了,nasm为开发者提供了一个一劳永逸的办法:宏汇编。这可是个好东西:
; kernel/_Interrupt.asm[bits 32]extern ISR_Handler; nasm的宏定义; 有错误号使用空指令%define ERROR_CODE nop; 没有错误号就Push无效错误号%define NO_ERROR_CODE push 0; %macro 宏函数 参数个数%macro ISR_CODE 2; 参数1:%1,参数2:%2,...[global ISR%1]ISR%1: cli ; 关闭中断 %2 ; 估计情况决定是否放置Push无效错误号 push byte %1 ; Push中断向量号 pusha mov ax,ds push eax ; 保存数据段描述符 mov ax,0x10 mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax push esp ; Registers_S指针 call ISR_Handler ; 调用相应中断处理函数这里可以使用,当前仅为测试 ; ISR_Handler%1的方法实现对不同中断的不同处理 ; 也可以使用[ISR_Handler + %1*4]函数指针数组的表示方法 add esp,4 pop ebx ; 恢复原来的数据段描述符 mov ds,bx mov es,bx mov fs,bx mov gs,bx mov ss,bx popa add esp,8 ; 跳过Error_Code iret ; 中断处理函数不能返回,需要使用iret或iretd打断;宏函数结束%endmacro;宏函数名 参数1,参数2,...ISR_CODE 0,NO_ERROR_CODEISR_CODE 1,NO_ERROR_CODEISR_CODE 2,NO_ERROR_CODEISR_CODE 3,NO_ERROR_CODEISR_CODE 4,NO_ERROR_CODEISR_CODE 5,NO_ERROR_CODEISR_CODE 6,NO_ERROR_CODEISR_CODE 7,NO_ERROR_CODEISR_CODE 8,ERROR_CODEISR_CODE 9,NO_ERROR_CODEISR_CODE 10,ERROR_CODEISR_CODE 11,ERROR_CODEISR_CODE 12,ERROR_CODEISR_CODE 13,ERROR_CODEISR_CODE 14,ERROR_CODEISR_CODE 15,NO_ERROR_CODEISR_CODE 16,NO_ERROR_CODEISR_CODE 17,ERROR_CODEISR_CODE 18,NO_ERROR_CODEISR_CODE 19,NO_ERROR_CODEISR_CODE 20,NO_ERROR_CODEISR_CODE 21,NO_ERROR_CODEISR_CODE 22,NO_ERROR_CODEISR_CODE 23,NO_ERROR_CODEISR_CODE 24,NO_ERROR_CODEISR_CODE 25,NO_ERROR_CODEISR_CODE 26,NO_ERROR_CODEISR_CODE 27,NO_ERROR_CODEISR_CODE 28,NO_ERROR_CODEISR_CODE 29,NO_ERROR_CODEISR_CODE 30,NO_ERROR_CODEISR_CODE 31,NO_ERROR_CODE
有了这些,C语言这边就小菜一碟了:
// kernel/interrupt.c#include #include #include IDT_S IDT[256];IDTR_S IDTR;static void init_IDT_Descriptor(uint16_t Segment_Selector, uint32_t Offset, uint16_t Type, IDT_S *IDT) { IDT->Offset0_15 = Offset & 0xffff; IDT->Segment_Selector = Segment_Selector; IDT->Type.All = Type; IDT->Offset16_31 = (Offset & 0xffff0000) >> 16; return;}void ISR_Handler(Registers_S *Registers) { printk(KERN_EMERG"InterruptNumber: %d\n", Registers->Interrupt_Number);}void init_IDT(void) { // 这里不加括号编译器会警告! IDTR.Limite = (sizeof(IDT_S) << 8) - 1; IDTR.Base = (uint32_t)&IDT; memset((uint8_t*)&IDT, 0, sizeof(IDT_S) << 8); init_IDT_Descriptor(0x08, (uint32_t)ISR0, INT_GATE, &IDT[0]); init_IDT_Descriptor(0x08, (uint32_t)ISR1, INT_GATE, &IDT[1]); init_IDT_Descriptor(0x08, (uint32_t)ISR2, INT_GATE, &IDT[2]); init_IDT_Descriptor(0x08, (uint32_t)ISR3, INT_GATE, &IDT[3]); init_IDT_Descriptor(0x08, (uint32_t)ISR4, INT_GATE, &IDT[4]); init_IDT_Descriptor(0x08, (uint32_t)ISR5, INT_GATE, &IDT[5]); init_IDT_Descriptor(0x08, (uint32_t)ISR6, INT_GATE, &IDT[6]); init_IDT_Descriptor(0x08, (uint32_t)ISR7, INT_GATE, &IDT[7]); init_IDT_Descriptor(0x08, (uint32_t)ISR8, INT_GATE, &IDT[8]); init_IDT_Descriptor(0x08, (uint32_t)ISR9, INT_GATE, &IDT[9]); init_IDT_Descriptor(0x08, (uint32_t)ISR10, INT_GATE, &IDT[10]); init_IDT_Descriptor(0x08, (uint32_t)ISR11, INT_GATE, &IDT[11]); init_IDT_Descriptor(0x08, (uint32_t)ISR12, INT_GATE, &IDT[12]); init_IDT_Descriptor(0x08, (uint32_t)ISR13, INT_GATE, &IDT[13]); init_IDT_Descriptor(0x08, (uint32_t)ISR14, INT_GATE, &IDT[14]); init_IDT_Descriptor(0x08, (uint32_t)ISR15, INT_GATE, &IDT[15]); init_IDT_Descriptor(0x08, (uint32_t)ISR16, INT_GATE, &IDT[16]); init_IDT_Descriptor(0x08, (uint32_t)ISR17, INT_GATE, &IDT[17]); init_IDT_Descriptor(0x08, (uint32_t)ISR18, INT_GATE, &IDT[18]); init_IDT_Descriptor(0x08, (uint32_t)ISR19, INT_GATE, &IDT[19]); init_IDT_Descriptor(0x08, (uint32_t)ISR20, INT_GATE, &IDT[20]); init_IDT_Descriptor(0x08, (uint32_t)ISR21, INT_GATE, &IDT[21]); init_IDT_Descriptor(0x08, (uint32_t)ISR22, INT_GATE, &IDT[22]); init_IDT_Descriptor(0x08, (uint32_t)ISR23, INT_GATE, &IDT[23]); init_IDT_Descriptor(0x08, (uint32_t)ISR24, INT_GATE, &IDT[24]); init_IDT_Descriptor(0x08, (uint32_t)ISR25, INT_GATE, &IDT[25]); init_IDT_Descriptor(0x08, (uint32_t)ISR26, INT_GATE, &IDT[26]); init_IDT_Descriptor(0x08, (uint32_t)ISR27, INT_GATE, &IDT[27]); init_IDT_Descriptor(0x08, (uint32_t)ISR28, INT_GATE, &IDT[28]); init_IDT_Descriptor(0x08, (uint32_t)ISR29, INT_GATE, &IDT[29]); init_IDT_Descriptor(0x08, (uint32_t)ISR30, INT_GATE, &IDT[30]); init_IDT_Descriptor(0x08, (uint32_t)ISR31, INT_GATE, &IDT[31]); // // 加载IDTR // __asm__ ("lidtl (IDTR)");}
下面当然是测试一下了:
看,编译一气呵成!
中断函数注册
每个中断函数肯定不一样,我们就可以使用函数指针数组,这种情况下,有些函数可以先不用实现:
// include/interrupt.h...typedef void (*Interrupt_Handler)(Registers_S*);void RegisterInterrupt(uint8_t Number, Interrupt_Handler Handler);...
// kernel/interrupt.c...Interrupt_Handler InterruptHandlers[256] = {NULL};...// 这里可以动态注册中断函数void RegisterInterrupt(uint8_t Number, Interrupt_Handler Handler) { InterruptHandlers[Number] = Handler; return;}// 这里保存函数尚未实现时不会调用空函数void ISR_Handler(Registers_S *Registers) { if (InterruptHandlers[Registers->Interrupt_Number] != NULL) { InterruptHandlers[Registers->Interrupt_Number](Registers); } else { printk(KERN_EMERG"InterruptNumber: %d\n", Registers->Interrupt_Number); }}...
运行结果还是一样的,今天就到这了!
关注"GuEes"公众号,了解更多消息
18f458中断入口C语言_操作系统开发之——中断相关推荐
- 当前操作系统缺少黑体等字体_操作系统开发之——中断
这里先提交一个代码的错误,之前运行过快,没看出刷屏的问题: // kernel/console.c...void init_console(void) {... // Before: // conso ...
- 单片机c语言 外部中断,单片机C语言代码:外部中断,按下中断按键LED不亮,LED1正常亮...
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 * 名称:外部中断 * 日期:2013-9-10 * 晶振:11.0592MHZ ************************************ ...
- 单片机定时器中断倒计时c语言,我用定时器中断控制倒计时30秒。程序不报错,但是显示一直停在30..请问好友们,帮我看看问题出在哪里?...
意味着我的定时器中断,的变量 i 没有发生变化,为什么呢? #include #define uchar unsigned char uchar code table[]={0xc0,0xf9,0xa ...
- 单片机串口中断控制流水灯c语言,基于单片机AT89C51控制的中断控制流水灯_课程设计报告...
<基于单片机AT89C51控制的中断控制流水灯_课程设计报告>由会员分享,可在线阅读,更多相关<基于单片机AT89C51控制的中断控制流水灯_课程设计报告(25页珍藏版)>请在 ...
- linux 在某个core上的中断 affinity c语言函数,Linux中断处理体系结构
各种的异常的C处理函数可以分为5类,他们分布在不同的文件中. 1.在arch/arm/kernel/trapsc.c中 未定义指令异常,总入口函数为do_undefinstr. 2.在arch/arm ...
- keil中断函数的写法_在 KeilC里,中断子程序与函数有何不同?( )_学小易找答案
[单选题]8051单片机共有( )个中断优先级 [单选题]对定时器 0 进行关中断操作,需要复位中断允许控制寄存器的: ( ) [多选题]真理向谬误转化的原因,主要在于( ) [多选题]"批 ...
- 51单片机中断的调用寄存器组(PSW)的作用,以及汇编堆栈的作用,堆栈指针的SP的使用方法,RAM的运行和ROM在单片机具体运行和C语言的优势和中断,定时器基础知识(上)
一,堆栈在汇编的作用,以及PUSH ACC 和 PUSH PSW 通过最简单的8051单片机RAM的分配可知,单片机从烧程序到达到一定的工作过程.首先明白为什么要引如"烧"写的过程 ...
- 使用设备树给DM9000网卡_触摸屏指定中断
目录 1 在设备树中描述网卡中断 2 dm9dev9000c.c 3 在设备树中描述触摸屏中断 1 在设备树中描述网卡中断 srom-cs4@20000000 {compatible = " ...
- iar环境下c语言编程,c语言_源代码-iar环境配置.pdf
c语言_源代码-iar环境配置 欢迎光临我的博客:/mikehendry 其实,IAR 编译环境的配置是相当重要的,没配置正确或者不符合自己的习惯的话,使用起来就会很麻烦.下面 我根据网上的经验和资料 ...
最新文章
- Harbor: 跨数据中心复制Docker镜像的开源实现
- Android Error:Could not run build action using Gradle installation
- MyBatis的架构和其运行流程
- sap.ca.ui.model.format.NumberFormat format logic for Globalization
- Spring Cloud 采用Consul做配置中心
- python pdf转word 表格_太赞了!Pdf转Word,我用Python 轻松搞定表格和水印!
- 美国国家公路交通安全管理局对特斯拉Autopilot系统展开正式调查
- 如何让Zen Cart 不在头部显示分类
- Andorid用ksoap2调用wcfDemo
- 利用linux内核代码玩转c链表
- netware php_在NetWare上搭建MySQL数据库_MySQL
- pinia 的使用(三)—— actions
- Angular 路由(routing)基本配置
- Combined Margin loss人脸识别训练笔记
- 华为hcie认证-链路聚合作用
- 【已解决】单个程序图标变白色怎么办?
- 数据标注是什么,如何进行数据标注?
- 【计算机图形学】流体模拟渲染基础
- 30以上java程序员出路,详细说明
- java实现图片上添加水印
热门文章
- 阿里云Redis混合存储典型场景:如何轻松搭建视频直播间系统
- “我今年 31 岁,工作 7 年,明年退休...”
- 什么是 CD 管道?一文告诉你如何借助Kubernetes、Ansible和Jenkins创建CD管道!
- 阿里云数字政府市场份额第一,同比增速102.57%
- Spark精华问答 | 为什么要学Spark?
- 【算法精讲】分享一道很不错的算法题
- 重装linux之后gcc等下载不了,Redhat linux下安装gcc
- SpringBoot 使用宝兰德中间件替换tomcat运行部署+控制台部署
- Directory /opt/jfrog/artifactory/var has bad permissions for user id 1030
- 设置axios默认访问前缀