手写简易操作系统(十七)--编写键盘驱动

这篇具有很好参考价值的文章主要介绍了手写简易操作系统(十七)--编写键盘驱动。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前情提要

上一节我们实现了锁与信号量,这一节我们就可以实现键盘驱动了,访问键盘输入的数据也属于临界区资源,所以需要锁的存在。

一、键盘简介

之前的 ps/2 键盘使用的是中断驱动的,在当时,按下键盘就会触发中断,引导操作系统去处理这个按键行文。但是当今的usb键盘,使用的是轮询机制,cpu会定时访问键盘看有没有按下键盘。

我个人认为这是cpu技术的进步导致的,在之前,cpu的频率比较低,使用轮询可能会导致漏掉用户按键的行为。但是在今天,cpu的主频已经非常高了,处理一个按键行为就触发中断,这个开销太大了,而且轮询的频率也上来了,现在每秒访问几千次对电脑一点影响都没有,所以现在大多采用了轮询机制。

不过据说中断驱动的还是比较快,现在一些电竞主板还是支持ps/2的接口,这个未经论证。

1.1、键盘的通码与断码

键盘的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键按下时的编码叫做通码,键盘上的触电接通了电路,使硬件产生了一个编码,故此通码叫makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode。

无论是按下键,或是松开键,当键的状态改变后,键盘中的8048芯片把按键对应的扫描码(通码或断码)发送到主板上的8042芯片,由8042处理后保存在自己的寄存器中,然后向8259A发送中断信号,这样处理器便去执行键盘中断处理程序,将8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。

1.2、键盘扫描码

键的扫描码是由键盘中的键盘编码器决定的,不同的编码方案便是不同的键盘扫描码,也就是说,相同的键在不同的编码方案下产生的通码和断码也是不同的。

根据不同的编码方案,键盘扫描码有三套,分别称为scan code set 1、scan code set 2、scan code set 3。

其中scan code set 1是XT键盘用的扫描码,这个历史就比较久远了。scan code set 2是AT键盘的扫描码,这个键盘和我们当今的键盘也不是很一样,但是已经比较接近了。scan code set 3是IBM PS/2系列高端计算机所用的键盘上,IBM蓝色巨人现在都凉了,这个键盘也就很少看到了。

第二套键盘扫描码几乎是目前所使用的键盘的标准,因此大多数键盘向8042发送的扫描码都是第二套扫描码。但是难免有别的键盘,所以才会出现8042这个芯片,这个芯片做一个中间层,为了兼容第一套键盘扫描码对应的中断处理程序,不管键盘用的是何种键盘扫描码,当键盘将扫描码发送到8042后,都由8042转换成第一套扫描码,我们再从8042中读取扫描码。

这里我们给出常用键位的扫描码(这里的扫描码就是通码,加0x80就是断码)

按键 扫描码 按键 扫描码 按键 扫描码
Esc 0x01 F1 0x3B F2 0x3C
F3 0x3D F4 0x3E F5 0x3F
F6 0x40 F7 0x41 F8 0x42
F9 0x43 F10 0x44 F11 0x57
F12 0x58 PrintSc 0x37 ScrollLk 0x46
Pause/Brk 0x45 ` 0x29 1 0x02
2 0x03 3 0x04 4 0x05
5 0x06 6 0x07 7 0x08
8 0x09 9 0x0A 0 0x0B
- 0x0C = 0x0D Backspace 0x0E
Tab 0x0F Q 0x10 W 0x11
E 0x12 R 0x13 T 0x14
Y 0x15 U 0x16 I 0x17
O 0x18 P 0x19 [ 0x1A
] 0x1B | 0x2B CapsLock 0x3A
A 0x1E S 0x1F D 0x20
F 0x21 G 0x22 H 0x23
J 0x24 K 0x25 L 0x26
; 0x27 0x28 Enter 0x1C
Shift左 0x2A Z 0x2C X 0x2D
C 0x2E V 0x2F B 0x30
N 0x31 M 0x32 , 0x33
. 0x34 / 0x35 Shift右 0x36
Ctrl左 0x1D Win左 0xE0 Alt左 0x38
Space 0x39 Alt右 0xE038 Win右 0xE0
Menu 0xE0 Ctrl右 0xE01D

问:为什么会有通码和断码,通码不就够了嘛

**答:**如果按一个组合键的话,比如ctrl+a,是先按下ctrl,再按a,再松开ctrl,再松开a。如果没有断码,我们无法判断ctrl是否松开。

1.3、键盘的芯片

和键盘相关的芯片只有8042和8048,它们都是独立的处理器,都有自己的寄存器和内存。Intel 8048芯片或兼容芯片位于键盘中,它是键盘编码器,Intel 8042芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的IO接口,因此它是8048的代理,也是前面所得到的处理器和键盘的“中间层”。我们只需要学习8042就够了

他的端口如下

寄存器 端口 读写
Output Buffer(输出缓冲区) 0x60
Input Buffer(输入缓冲区) 0x60
Status Register(状态寄存器) 0x64
Control Register(控制寄存器) 0x64

状态寄存器8位宽度的寄存器,只读,反映8048和8042的内部工作状态。各位意义如下。

(1)位0:置1时表示输出缓冲区寄存器已满,处理器通过in指令读取后该位自动置0。
(2)位1:置1时表示输入缓冲区寄存器已满,8042将值读取后该位自动置0。
(3)位2:系统标志位,最初加电时为0,自检通过后置为1。
(4)位3:置1时,表示输入缓冲区中的内容是命令,置0时,输入缓冲区中的内容是普通数据。
(5)位4:置1时表示键盘启用,置0时表示键盘禁用。
(6)位5:置1时表示发送超时。
(7)位6:置1时表示接收超时。
(8)位7:来自8048的数据在奇偶校验时出错。

8位宽度的寄存器,只写,用于写入命令控制字。每个位都可以设置一种工作方式,意义如下。

(1)位0:置1时启用键盘中断。
(2)位1:置1时启用鼠标中断。
(3)位2:设置状态寄存器的位2。
(4)位3:置1时,状态寄存器的位4无效。
(5)位4:置1时禁止键盘。
(6)位5:置1时禁止鼠标。
(7)位6:将第二套键盘扫描码转换为第一套键盘扫描码。
(8)位7:保留位,默认为0。

二、环形队列

键盘中断的数据是放在队列中的,等待其他线程的读取。如果我们之前做过关于软件相关的工作,很容易理解这个概念,就是buffer,缓冲区。因为我们是一直在输入的,所以这里设计成了环形队列。

我们看一下环形队列的数据结构

#define bufsize 256

/* 环形队列 */
struct ioqueue {
    // 生产者消费者问题
    struct lock lock;
    // 生产者,缓冲区不满时就继续往里面放数据
    struct task_struct* producer;
    // 消费者,缓冲区不空时就继续从往里面拿数据
    struct task_struct* consumer;
    char buf[bufsize];			// 缓冲区大小
    int32_t head;			    // 队首,数据往队首处写入
    int32_t tail;			    // 队尾,数据从队尾处读出
};

这个就很明朗了。一个生产者一个消费者,生产者向buf中添加数据,消费者从buf中取出数据,为了防止buf中的数据出错,生产者和消费者同时只能有一个可以访问到buf。如果buf中数据满了,生产者就不能放了,此时阻塞生产者,如果buf中数据为空,消费者就不能拿了,此时阻塞消费者。

我们看一下具体的实现

/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {
    lock_init(&ioq->lock);                 // 初始化io队列的锁
    ioq->producer = ioq->consumer = NULL;  // 生产者和消费者置空
    ioq->head = ioq->tail = 0;             // 队列的首尾指针指向缓冲区数组第0个位置
}

/* 返回pos在缓冲区中的下一个位置值 */
static inline int32_t next_pos(int32_t pos) {
    return (pos + 1) % bufsize;
}

/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {
    return next_pos(ioq->head) == ioq->tail;
}

/* 判断队列是否已空 */
bool ioq_empty(struct ioqueue* ioq) {
    return ioq->head == ioq->tail;
}

/* 使当前生产者或消费者在此缓冲区上等待 */
static void ioq_wait(struct task_struct** waiter) {
    // 二级指针不为空,指向的pcb指针地址为空
    ASSERT(*waiter == NULL && waiter != NULL);
    *waiter = running_thread();
    thread_block(TASK_BLOCKED);
}

/* 唤醒waiter */
static void wakeup(struct task_struct** waiter) {
    // 二级指针指向不为空
    ASSERT(*waiter != NULL);
    thread_unblock(*waiter);
    *waiter = NULL;
}

/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue* ioq) {
    // 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,等待生产者唤醒
    while (ioq_empty(ioq)) {
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq->consumer);
        lock_release(&ioq->lock);
    }

    char byte = ioq->buf[ioq->tail];	  // 从缓冲区中取出
    ioq->tail = next_pos(ioq->tail);	  // 把读游标移到下一位置

    if (ioq->producer != NULL) {
        wakeup(&ioq->producer);		      // 唤醒生产者
    }

    return byte;
}

/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {
    // 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,等待消费者线程唤醒自己
    while (ioq_full(ioq)) {
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq->producer);
        lock_release(&ioq->lock);
    }
    ioq->buf[ioq->head] = byte;      // 把字节放入缓冲区中
    ioq->head = next_pos(ioq->head); // 把写游标移到下一位置

    if (ioq->consumer != NULL) {
        wakeup(&ioq->consumer);      // 唤醒消费者
    }
}

我们看一下后面两个函数,waitwakeup,这两个函数,这两个函数传入的是一个pcb指针的地址,所以这里是一个二级指针。所以无论是阻塞还是解除阻塞都是取这个二级指针的地址,也就得到了pcb指针。这里对于不熟悉指针的人来说可能会有点扰。

三、键盘驱动

#define KBD_BUF_PORT 0x60	 // 键盘buffer寄存器端口号为0x60

/* 用转义字符定义部分控制字符 */
#define esc		    '\033'	 // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace	'\b'
#define tab		    '\t'
#define enter		'\r'
#define delete		'\177'	 // 八进制表示字符,十六进制为'\x7f'

/* 以上不可见字符一律定义为0 */
#define char_invisible	0
#define ctrl_l_char	    char_invisible
#define ctrl_r_char	    char_invisible
#define shift_l_char    char_invisible
#define shift_r_char	char_invisible
#define alt_l_char	    char_invisible
#define alt_r_char	    char_invisible
#define caps_lock_char	char_invisible

/* 定义控制字符的通码和断码 */
#define shift_l_make	0x2a
#define shift_r_make 	0x36 
#define alt_l_make   	0x38
#define alt_r_make   	0xe038
#define alt_r_break   	0xe0b8
#define ctrl_l_make  	0x1d
#define ctrl_r_make  	0xe01d
#define ctrl_r_break 	0xe09d
#define caps_lock_make 	0x3a

struct ioqueue kbd_buf;	   // 定义键盘缓冲区

/* 定义以下变量记录相应键是否按下的状态,
 * ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;

/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
    /* 扫描码   未与shift组合  与shift组合*/
    /* ---------------------------------- */
    /* 0x00 */	{0,	0},
    /* 0x01 */	{esc,	esc},
    /* 0x02 */	{'1',	'!'},
    /* 0x03 */	{'2',	'@'},
    /* 0x04 */	{'3',	'#'},
    /* 0x05 */	{'4',	'$'},
    /* 0x06 */	{'5',	'%'},
    /* 0x07 */	{'6',	'^'},
    /* 0x08 */	{'7',	'&'},
    /* 0x09 */	{'8',	'*'},
    /* 0x0A */	{'9',	'('},
    /* 0x0B */	{'0',	')'},
    /* 0x0C */	{'-',	'_'},
    /* 0x0D */	{'=',	'+'},
    /* 0x0E */	{backspace, backspace},
    /* 0x0F */	{tab,	tab},
    /* 0x10 */	{'q',	'Q'},
    /* 0x11 */	{'w',	'W'},
    /* 0x12 */	{'e',	'E'},
    /* 0x13 */	{'r',	'R'},
    /* 0x14 */	{'t',	'T'},
    /* 0x15 */	{'y',	'Y'},
    /* 0x16 */	{'u',	'U'},
    /* 0x17 */	{'i',	'I'},
    /* 0x18 */	{'o',	'O'},
    /* 0x19 */	{'p',	'P'},
    /* 0x1A */	{'[',	'{'},
    /* 0x1B */	{']',	'}'},
    /* 0x1C */	{enter,  enter},
    /* 0x1D */	{ctrl_l_char, ctrl_l_char},
    /* 0x1E */	{'a',	'A'},
    /* 0x1F */	{'s',	'S'},
    /* 0x20 */	{'d',	'D'},
    /* 0x21 */	{'f',	'F'},
    /* 0x22 */	{'g',	'G'},
    /* 0x23 */	{'h',	'H'},
    /* 0x24 */	{'j',	'J'},
    /* 0x25 */	{'k',	'K'},
    /* 0x26 */	{'l',	'L'},
    /* 0x27 */	{';',	':'},
    /* 0x28 */	{'\'',	'"'},
    /* 0x29 */	{'`',	'~'},
    /* 0x2A */	{shift_l_char, shift_l_char},
    /* 0x2B */	{'\\',	'|'},
    /* 0x2C */	{'z',	'Z'},
    /* 0x2D */	{'x',	'X'},
    /* 0x2E */	{'c',	'C'},
    /* 0x2F */	{'v',	'V'},
    /* 0x30 */	{'b',	'B'},
    /* 0x31 */	{'n',	'N'},
    /* 0x32 */	{'m',	'M'},
    /* 0x33 */	{',',	'<'},
    /* 0x34 */	{'.',	'>'},
    /* 0x35 */	{'/',	'?'},
    /* 0x36	*/	{shift_r_char, shift_r_char},
    /* 0x37 */	{'*',	'*'},
    /* 0x38 */	{alt_l_char, alt_l_char},
    /* 0x39 */	{' ',	' '},
    /* 0x3A */	{caps_lock_char, caps_lock_char}
    /*其它按键暂不处理*/
};

/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {

    /* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
    bool ctrl_down_last = ctrl_status;
    bool shift_down_last = shift_status;
    bool caps_lock_last = caps_lock_status;

    uint16_t scancode = inb(KBD_BUF_PORT);

    // 若扫描码是e0开头的, 结束此次中断处理函数,等待下一个扫描码进来
    if (scancode == 0xe0) {
        ext_scancode = true;    // 打开e0标记
        return;
    }

    // 如果上次是以0xe0开头,将扫描码合并
    if (ext_scancode) {
        scancode = ((0xe000) | scancode);
        ext_scancode = false;   // 关闭e0标记
    }

    // 若是断码(按键弹起时产生的扫描码)
    if ((scancode & 0x0080) != 0) {
        // 获得相应的通码
        uint16_t make_code = (scancode &= 0xff7f);
        // 若是任意以下三个键弹起了,将状态置为false
        if (make_code == ctrl_l_make || make_code == ctrl_r_make) {
            ctrl_status = false;
        }
        else if (make_code == shift_l_make || make_code == shift_r_make) {
            shift_status = false;
        }
        else if (make_code == alt_l_make || make_code == alt_r_make) {
            alt_status = false;
        }
        // 若是其他非控制键位,不需要处理,那些键位我们只需要知道通码
        return;

    }
    // 若是通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code
    else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)) {
        // keymap的二维索引
        bool shift = false;
        // 按下的键不是字母
        if ((scancode < 0x0e) || (scancode == 0x29) || \
            (scancode == 0x1a) || (scancode == 0x1b) || \
            (scancode == 0x2b) || (scancode == 0x27) || \
            (scancode == 0x28) || (scancode == 0x33) || \
            (scancode == 0x34) || (scancode == 0x35)) {  
            if (shift_down_last) {
                shift = true;
            }
        }
        // 如果按下的键是字母,需要和CapsLock配合
        else {
            if (shift_down_last && caps_lock_last) {      // 如果shift和capslock同时按下
                shift = false;
            }
            else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下
                shift = true;
            }
            else {
                shift = false;
            }
        }

        // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
        uint8_t index = (scancode &= 0x00ff);
        // 在数组中找到对应的字符
        char cur_char = keymap[index][shift];

        // 如果cur_char不为0,也就是ascii码为除'\0'外的字符就加入键盘缓冲区中
        if (cur_char) {
            // 如果ctrl按下,且输入的字符为‘l’或者‘u’,那就保存为 cur_char-‘a’,主要是‘a’前面26位没啥用
            if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {
                cur_char -= 'a';
            }

            // 如果缓冲区未满,就将其加入缓冲区
            if (!ioq_full(&kbd_buf)) {
                ioq_putchar(&kbd_buf, cur_char);
            }
            return;
        }

        // 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键
        if (scancode == ctrl_l_make || scancode == ctrl_r_make) {
            ctrl_status = true;
        }
        else if (scancode == shift_l_make || scancode == shift_r_make) {
            shift_status = true;
        }
        else if (scancode == alt_l_make || scancode == alt_r_make) {
            alt_status = true;
        }
        // 这里注意,大写的锁定键是取反
        else if (scancode == caps_lock_make) {
            caps_lock_status = !caps_lock_status;
        }
    }
    else {
        put_str("unknown key\n");
    }
}

/* 键盘初始化 */
void keyboard_init() {
    put_str("keyboard init start\n");
    ioqueue_init(&kbd_buf);
    register_handler(0x21, intr_keyboard_handler);
    put_str("keyboard init done\n");
}

键盘驱动就稍显复杂一点,主要是涉及到了shiftctrlaltcaplock这些个控制键,这些键位是否按下所表示的通码断码是不一样的。这里就是处理字符,相信大家看代码就可以看明白。

四、仿真

我们创建一个线程,键盘输入什么,打印什么

手写简易操作系统(十七)--编写键盘驱动,手写简易操作系统,计算机外设,单片机,嵌入式硬件,操作系统,x86

结束语

本节我们编写了键盘驱动以及其使用的环形队列数据结构。下一节我们将实现一个用户进程,即特权级为3的进程。

老规矩,代码地址为 https://github.com/lyajpunov/os文章来源地址https://www.toymoban.com/news/detail-858539.html

到了这里,关于手写简易操作系统(十七)--编写键盘驱动的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 如何编写Windows操作系统

    编写一个完整的操作系统是一项非常复杂的任务,需要深入了解计算机体系结构和操作系统的工作原理,还需要熟悉汇编语言和C/C++编程语言。在这里,我们简单介绍一下编写Windows操作系统的基本步骤。 Windows操作系统是由微软公司开发的,因此微软提供了一些用于Windows操作

    2024年02月07日
    浏览(45)
  • 【C语言】简易登录注册系统(登录、注册、改密、文件操作)

            本登录注册系统通过使用C语言中的结构体、函数、文件操作以及指针等,设计与实现了一个小型用户登录注册系统的登录、注册、修改密码等基本功能。         本系统全部功能基本运行良好、用户界面友好、操作简单、使用方便。但系统仍然有不完善之处。例如

    2024年02月03日
    浏览(30)
  • 简易操作系统:使用Python 做的图形界面 C 做的内核

    目录 实验要求 一、文件管理和用户接口 ⑴文件的逻辑结构 ⑵磁盘模拟 ⑶目录结构 ⑷磁盘分配 ⑸磁盘空闲存储空间管理 ⑹用户接口 ⑺屏幕显示  代码部分         python调用c的方法: ​编辑 c语言部分,文件名 Operating_System_C.c python语言部分 运行实例:    文件管理和用户

    2024年02月08日
    浏览(38)
  • C# 编写的 64位操作系统 - MOOS

    MOOS ( My Own Operating System ) 是一个使用 .NET Native AOT 技术编译的C# 64位操作系统。项目地址:https://github.com/nifanfa/MOOS。 微软MVP实验室研究员 关于编译 MOOS 的信息,请阅读编译维基页面:https://github.com/nifanfa/MOOS/wiki/ VMware Workstation Player https://www.vmware.com/products/workstation-player.htm

    2024年02月05日
    浏览(39)
  • Linux下C程序的编写(操作系统实验)

    实验题目:   Linux下C程序的编写                            实验目的:   (1)掌握Linux下C程序的编写、编译与运行方法; (2)掌握gcc编译器的编译过程,熟悉编译的各个阶段;        (3)熟悉Makefile文件的编写格式和make编译工具的使用方法。        

    2024年04月28日
    浏览(41)
  • 飞腾笔记本/银河麒麟桌面操作系统键盘无法使用

    在安装完银河麒麟V10完成以后,进入系统后无法使用键盘,外接键盘以及在安装系统的过程中均可正常使用。 因为在安装过程中,以及外接键盘均可正常使用,所以初步怀疑是笔记本键盘与系统之间的不兼容导致 将/boot/grub.cfg配置文件中的drivicetree/dtb/u-boot-general.dtb修改为d

    2024年02月12日
    浏览(84)
  • 键盘敲入 A 字母时,操作系统期间发生了什么?

    关于8.1 键盘敲入 A 字母时,操作系统期间发生了什么?的总结,前面都介绍了,但是在最后总结操作系统发生了什么的时候,我觉得有点不详细,于是写一写自己的补充和理解,不一定正确。 键盘敲击之后, 键盘控制器根据敲击的键生成扫描码,写入寄存器 。 同时通过中

    2024年02月11日
    浏览(28)
  • ubuntu版本Linux操作系统上安装键盘中文输入法

    要在ubuntu版本Linux操作系统上安装键盘中文输入法 可以按照以下步骤进行操作: 1、Linux终端输入: sudo apt-get install ibus-pinyin 这将安装一个常用的中文输入法 “ ibus-pinyin ”。 2、重新启动系统:为了使输入法生效,需要重新启动您的系统 Linux终端输入: sudo reboot 3、在重启后

    2024年02月16日
    浏览(39)
  • 基于JavaSE+JDBC使用控制台操作的简易购物系统【源码+数据库】

    本项目是一套基于JavaSE+JDBC使用控制台操作的简易购物系统,主要针对计算机相关专业的正在做bishe的学生和需要项目实战练习的Java学习者。 包含:项目源码、数据库脚本等,该项目可以直接作为bishe使用。 项目都经过严格调试,确保可以运行! JavaSE+JDBC+idea+mysql 用户角色:

    2024年02月04日
    浏览(36)
  • 【linux深入剖析】操作系统与用户之间的接口:自定义简易shell制作全过程

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 Linux的Shell是一种命令行解释器,它是用户与操作系统内核之间的接口。 通过Shell,用户可以输入命令并与

    2024年03月18日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包