Linux设备驱动程序学习笔记——第十章 中断处理
2021/5/17 7:27:49
本文主要是介绍Linux设备驱动程序学习笔记——第十章 中断处理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Linux设备驱动程序学习笔记
第十章 中断处理
一、安装中断处理例程
中断信号线是珍贵且有限的资源。内核维护了一个中断信号线的注册表,模块在使用中断前要现请求一个中断通道(或者中断请求IRQ),然后在使用后释放该通道。
#include <linux/sched.h> int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) //irq要申请的中断号 //irq_handler_t handler,typedef irqreturn_t (*irq_handler_t)(int, void *);这里是要安装的中断处理函数指针,后面讨论 //unsigned long flags中断管理有关的位掩码选项 //const char *name传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者(参见后面) //void *dev用于共享的中断信号线。唯一的标识符,在中断信号线空闲时可以使用它,驱动程序也可以使用它指向驱动程序自己的私有数据区(用来识别哪个设备产生中断) void free_irq(unsigned int irq, void *dev);
request_irq的正确位置是在设备的第一次打开、硬件被告知产生中断之前。
free_irq是最后一次关闭设备,硬件被告知不再用中断处理器之后。
if (short_irq >= 0) { result = request_irq(short_irq, short_interrupt, 0, "short", NULL); if (result) { printk(KERN_INFO "short: can't get assigned irq %i\n", short_irq); short_irq = -1; } else { /* actually enable it -- assume this *is* a parallel port */ outb(0x10,short_base+2); } }
/proc接口
当硬件的中断到达处理器时,一个内部计数器递增,这位检查设备提供了方法
- cat /proc/interrupts
显示各个CPU在IRQ号上的发生的中断数量。但其只会显示那些已经安装了中断处理例程的中断。若有些中断处理例程时打开文件时安装则不会显示 - cat /proc/stat
一般比上一个方式更有用,可以显示所有中断号发生次数。并且依赖于体系结构。
自动监测IRQ号
驱动初始化时最迫切的问题之一就是如何确定设备将要使用哪一条IRQ信号线。因为用户大部分时间不知道指定哪个中断号。
大多数现在硬件设备有能力告诉驱动程序它将使用的中断线,比如PCI标准要求外设声明它们打算是用的中断线,但有些设备需要自动监测。
两种方法:
- 内核帮助下的探测
void short_kernelprobe(void) { int count = 0; do { unsigned long mask; mask = probe_irq_on();//返回一个未分配中断的位掩码,用于传递该后面的probe_irq_off函数。调用该函数之后,驱动程序要安排设备产生至少一次中断 outb_p(0x10,short_base+2); /* 启用中断报告 */ outb_p(0x00,short_base); /* 清除该位 */ outb_p(0xFF,short_base); /* 设置该位:中断! */ outb_p(0x00,short_base+2); /* 禁止中断报告 */ udelay(5); /* 留给中断探测一些时间 */ short_irq = probe_irq_off(mask); if (short_irq == 0) { /* none of them? */ printk(KERN_INFO "short: no irq reported by probe\n"); short_irq = -1; } /* * 如果失败,重新尝试5次后放弃 */ } while (short_irq < 0 && count++ < 5); if (short_irq < 0) printk("short: probe failed %i times, giving up\n", count); }
- DIY探测
和上述原理一样,启用所有未被占用的中断,然后观察会发生什么。不同的是,应充分发挥对有关设备的了解,如例,我们假定可能的IRQ值是3,5,7,9
volatile int short_irq = -1; irqreturn_t short_probing(int irq, void *dev_id) { if (short_irq == 0) short_irq = irq; /* found */ if (short_irq != irq) short_irq = -irq; /* ambiguous */ return IRQ_HANDLED; } void short_selfprobe(void) { int trials[] = {3, 5, 7, 9, 0}; int tried[] = {0, 0, 0, 0, 0}; int i, count = 0; /* * install the probing handler for all possible lines. Remember * the result (0 for success, or -EBUSY) in order to only free * what has been acquired */ for (i = 0; trials[i]; i++) tried[i] = request_irq(trials[i], short_probing, 0, "short probe", NULL); do { short_irq = 0; /* none got, yet */ outb_p(0x10,short_base+2); /* enable */ outb_p(0x00,short_base); outb_p(0xFF,short_base); /* toggle the bit */ outb_p(0x00,short_base+2); /* disable */ udelay(5); /* give it some time */ /* the value has been set by the handler */ if (short_irq == 0) { /* none of them? */ printk(KERN_INFO "short: no irq reported by probe\n"); } /* * If more than one line has been activated, the result is * negative. We should service the interrupt (but the lpt port * doesn't need it) and loop over again. Do it at most 5 times */ } while (short_irq <=0 && count++ < 5); /* end of loop, uninstall the handler */ for (i = 0; trials[i]; i++) if (tried[i] == 0) free_irq(trials[i], NULL); if (short_irq < 0) printk("short: probe failed %i times, giving up\n", count); }
快速和慢速处理例程
快中断SA_INTERRUPT执行时,当前处理器上其他所有中断被禁止,但其他处理器仍可处理中断。但在现代系统中,SA_INTERRUPT只在少数几种特殊情况如定时器中断使用,驱动开发应尽量不使用。
二、实现中断处理例程
特殊在于例程是在中断时间内运行的,行为受到一些限制:
处理例程不能向用户空间发送和接受数据,因为他不是在任何进程的上下文中执行。
处理例程不能做任何可能发生休眠的操作,如调用wait_event使用不带GFP_ATOMIC标志的内存分配操作,或者锁住一个信号量等等。
处理例程不能调用schdule函数。
三、启用和禁用中断
略
四、顶半部和底半部
tasklet通常是底半部处理的优选机制,这种机制非常快,但是所有tasklet代码必须是原子的。
工作队列具有更高的延迟,但允许休眠。
tasklet
tasklet是软中断的一种特殊函数,可以被多次调度运行,但tasklet的调度不会累积。
不会有同一个tasklet的多个实例并行地运行,但是可以与其他tasklet并行运行在SMP系统上。
声明:
DECLARE_TASKLET(name, func, data);
name是给tasklet的名字,func是执行tasklet时调用的函数(void (*func)(unsigned long);),data是一个用来传递给tasklet函数的unsigned long类型的值
tasklet_schedule函数用来调度一个tasklet运行。
void tasklet_schedule(struct tasklet_struct *t)
void short_do_tasklet(struct tasklet_struct *); DECLARE_TASKLET(short_tasklet, short_do_tasklet);//Linux5.9版本更新 irqreturn_t short_tl_interrupt(int irq, void *dev_id) { ktime_get_real_ts64((struct timespec64 *) tv_head); /* cast to stop 'volatile' warning */ short_incr_tv(&tv_head); tasklet_schedule(&short_tasklet); short_wq_count++; /* record that an interrupt arrived */ return IRQ_HANDLED; } void short_do_tasklet (struct tasklet_struct * unused) #endif { int savecount = short_wq_count, written; short_wq_count = 0; /* we have already been removed from the queue */ /* * The bottom half reads the tv array, filled by the top half, * and prints it to the circular text buffer, which is then consumed * by reading processes */ /* First write the number of interrupts that occurred before this bh */ written = sprintf((char *)short_head,"bh after %6i\n",savecount); short_incr_bp(&short_head, written); /* * Then, write the time values. Write exactly 16 bytes at a time, * so it aligns with PAGE_SIZE */ do { written = sprintf((char *)short_head,"%08u.%06lu\n", (int)(tv_tail->tv_sec % 100000000), (int)(tv_tail->tv_nsec) / NSEC_PER_USEC); short_incr_bp(&short_head, written); short_incr_tv(&tv_tail); } while (tv_tail != tv_head); wake_up_interruptible(&short_queue); /* awake any reading process */ }
该tasklet记录了自它上次被调用以来产生的中断次数。
工作队列
static struct work_struct short_wq; irqreturn_t short_wq_interrupt(int irq, void *dev_id) { /* Grab the current time information. */ ktime_get_real_ts64((struct timespec64 *) tv_head); short_incr_tv(&tv_head); /* Queue the bh. Don't worry about multiple enqueueing */ schedule_work(&short_wq); short_wq_count++; /* record that an interrupt arrived */ return IRQ_HANDLED; } void short_do_tasklet (struct tasklet_struct * unused) #endif { int savecount = short_wq_count, written; short_wq_count = 0; /* we have already been removed from the queue */ /* * The bottom half reads the tv array, filled by the top half, * and prints it to the circular text buffer, which is then consumed * by reading processes */ /* First write the number of interrupts that occurred before this bh */ written = sprintf((char *)short_head,"bh after %6i\n",savecount); short_incr_bp(&short_head, written); /* * Then, write the time values. Write exactly 16 bytes at a time, * so it aligns with PAGE_SIZE */ do { written = sprintf((char *)short_head,"%08u.%06lu\n", (int)(tv_tail->tv_sec % 100000000), (int)(tv_tail->tv_nsec) / NSEC_PER_USEC); short_incr_bp(&short_head, written); short_incr_tv(&tv_tail); } while (tv_tail != tv_head); wake_up_interruptible(&short_queue); /* awake any reading process */ } int short_init(void){ /*......*/ INIT_WORK(&short_wq, (void (*)(struct work_struct *)) short_do_tasklet); if (short_irq >= 0 && (wq + tasklet) > 0) { free_irq(short_irq,NULL); result = request_irq(short_irq, tasklet ? short_tl_interrupt : short_wq_interrupt, 0, "short-bh", NULL); if (result) { printk(KERN_INFO "short-bh: can't get assigned irq %i\n", short_irq); short_irq = -1; } } }
五、中断共享
安装共享的处理例程
与普通非共享中断的不同:
- 请求中断时,必须指定flags参数中的SA_SHIRQ位
- dev_id参数必须是唯一的。任何指向模块地址空间的指针都可以使用,但dev_id不能设置为NULL
两种情况request_irq会成功: - 中断信号线空闲
- 任何已经注册了该中断信号线的处理例程也标示了IRQ是共享的
free_irq就是通过dev_id
/proc接口和共享的中断
/proc/stat不会有影响,proc/interrupts会改变
即共享中断例程会共同显示在同一行
六、中断驱动的I/O
/* * A version of the "short" driver which drives a parallel printer directly, * with a lot of simplifying assumptions. * * Copyright (C) 2001 Alessandro Rubini and Jonathan Corbet * Copyright (C) 2001 O'Reilly & Associates * * The source code in this file can be freely used, adapted, * and redistributed in source or binary form, so long as an * acknowledgment appears in derived source files. The citation * should list that the code comes from the book "Linux Device * Drivers" by Alessandro Rubini and Jonathan Corbet, published * by O'Reilly & Associates. No warranty is attached; * we cannot take responsibility for errors or fitness for use. * * $Id: shortprint.c,v 1.4 2004/09/26 08:01:04 gregkh Exp $ */ #include <linux/module.h> #include <linux/moduleparam.h> #include <linux/sched.h> #include <linux/sched/signal.h> #include <linux/kernel.h> /* printk() */ #include <linux/fs.h> /* everything... */ #include <linux/errno.h> /* error codes */ #include <linux/delay.h> /* udelay */ #include <linux/slab.h> #include <linux/ioport.h> #include <linux/interrupt.h> #include <linux/workqueue.h> #include <linux/timer.h> #include <linux/poll.h> #include <asm/io.h> #include <linux/semaphore.h> #include <asm/atomic.h> #include "shortprint.h" #define SHORTP_NR_PORTS 3 /* * all of the parameters have no "shortp_" prefix, to save typing when * specifying them at load time */ static int major = 0; /* dynamic by default */ module_param(major, int, 0); /* default is the first printer port on PC's. "shortp_base" is there too because it's what we want to use in the code */ static unsigned long base = 0x378; unsigned long shortp_base = 0; module_param(base, long, 0); /* The interrupt line is undefined by default. "shortp_irq" is as above */ static int irq = -1; static int shortp_irq = -1; module_param(irq, int, 0); /* Microsecond delay around strobe. */ static int delay = 0; static int shortp_delay; module_param(delay, int, 0); MODULE_AUTHOR ("Jonathan Corbet"); MODULE_LICENSE("Dual BSD/GPL"); /* * Forwards. */ static void shortp_cleanup(void); static void shortp_timeout(struct timer_list *unused); /* * Input is managed through a simple circular buffer which, among other things, * is allowed to overrun if the reader isn't fast enough. That makes life simple * on the "read" interrupt side, where we don't want to block. */ static unsigned long shortp_in_buffer = 0; static unsigned long volatile shortp_in_head; static volatile unsigned long shortp_in_tail; DECLARE_WAIT_QUEUE_HEAD(shortp_in_queue); static struct timespec64 shortp_tv; /* When the interrupt happened. */ /* * Atomicly increment an index into shortp_in_buffer */ static inline void shortp_incr_bp(volatile unsigned long *index, int delta) { unsigned long new = *index + delta; barrier (); /* Don't optimize these two together */ *index = (new >= (shortp_in_buffer + PAGE_SIZE)) ? shortp_in_buffer : new; } /* * On the write side we have to be more careful, since we don't want to drop * data. The semaphore is used to serialize write-side access to the buffer; * there is only one consumer, so read-side access is unregulated. The * wait queue will be awakened when space becomes available in the buffer. */ static unsigned char *shortp_out_buffer = NULL; static volatile unsigned char *shortp_out_head, *shortp_out_tail; static struct mutex shortp_out_mutex; static DECLARE_WAIT_QUEUE_HEAD(shortp_out_queue); /* * Feeding the output queue to the device is handled by way of a * workqueue. */ static void shortp_do_work(struct work_struct *work); static DECLARE_WORK(shortp_work, shortp_do_work); static struct workqueue_struct *shortp_workqueue; /* * Available space in the output buffer; should be called with the semaphore * held. Returns contiguous space, so caller need not worry about wraps. */ static inline int shortp_out_space(void) { if (shortp_out_head >= shortp_out_tail) { int space = PAGE_SIZE - (shortp_out_head - shortp_out_buffer); return (shortp_out_tail == shortp_out_buffer) ? space - 1 : space; } else return (shortp_out_tail - shortp_out_head) - 1; } static inline void shortp_incr_out_bp(volatile unsigned char **bp, int incr) { unsigned char *new = (unsigned char *) *bp + incr; if (new >= (shortp_out_buffer + PAGE_SIZE)) new -= PAGE_SIZE; *bp = new; } /* * The output "process" is controlled by a spin lock; decisions on * shortp_output_active or manipulation of shortp_out_tail require * that this lock be held. */ static spinlock_t shortp_out_lock; volatile static int shortp_output_active; DECLARE_WAIT_QUEUE_HEAD(shortp_empty_queue); /* waked when queue empties */ /* * When output is active, the timer is too, in case we miss interrupts. Hold * shortp_out_lock if you mess with the timer. */ static struct timer_list shortp_timer; #define TIMEOUT 5*HZ /* Wait a long time */ /* * Open the device. */ static int shortp_open(struct inode *inode, struct file *filp) { return 0; } static int shortp_release(struct inode *inode, struct file *filp) { /* Wait for any pending output to complete */ wait_event_interruptible(shortp_empty_queue, shortp_output_active==0); return 0; } static unsigned int shortp_poll(struct file *filp, poll_table *wait) { return POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM; } /* * The read routine, which doesn't return data from the device; instead, it * returns timing information just like the "short" device. */ static ssize_t shortp_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { int count0; DEFINE_WAIT(wait); while (shortp_in_head == shortp_in_tail) { prepare_to_wait(&shortp_in_queue, &wait, TASK_INTERRUPTIBLE); if (shortp_in_head == shortp_in_tail) schedule(); finish_wait(&shortp_in_queue, &wait); if (signal_pending (current)) /* a signal arrived */ return -ERESTARTSYS; /* tell the fs layer to handle it */ } /* count0 is the number of readable data bytes */ count0 = shortp_in_head - shortp_in_tail; if (count0 < 0) /* wrapped */ count0 = shortp_in_buffer + PAGE_SIZE - shortp_in_tail; if (count0 < count) count = count0; if (copy_to_user(buf, (char *)shortp_in_tail, count)) return -EFAULT; shortp_incr_bp(&shortp_in_tail, count); return count; } /* * Wait for the printer to be ready; this can sleep. */ static void shortp_wait(void) { if ((inb(shortp_base + SP_STATUS) & SP_SR_BUSY) == 0) { printk(KERN_INFO "shortprint: waiting for printer busy\n"); printk(KERN_INFO "Status is 0x%x\n", inb(shortp_base + SP_STATUS)); while ((inb(shortp_base + SP_STATUS) & SP_SR_BUSY) == 0) { set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(10*HZ); } } } /* * Write the next character from the buffer. There should *be* a next * character... The spinlock should be held when this routine is called. */ static void shortp_do_write(void) { unsigned char cr = inb(shortp_base + SP_CONTROL); /* Something happened; reset the timer */ mod_timer(&shortp_timer, jiffies + TIMEOUT); /* Strobe a byte out to the device */ outb_p(*shortp_out_tail, shortp_base+SP_DATA); shortp_incr_out_bp(&shortp_out_tail, 1); if (shortp_delay) udelay(shortp_delay); outb_p(cr | SP_CR_STROBE, shortp_base+SP_CONTROL); if (shortp_delay) udelay(shortp_delay); outb_p(cr & ~SP_CR_STROBE, shortp_base+SP_CONTROL); } /* * Start output; call under lock. */ static void shortp_start_output(void) { if (shortp_output_active) /* Should never happen */ return; /* Set up our 'missed interrupt' timer */ shortp_output_active = 1; shortp_timer.expires = jiffies + TIMEOUT; add_timer(&shortp_timer); /* And get the process going. */ queue_work(shortp_workqueue, &shortp_work); } /* * Write to the device. */ static ssize_t shortp_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { int space, written = 0; unsigned long flags; /* * Take and hold the mutex for the entire duration of the operation. The * consumer side ignores it, and it will keep other data from interleaving * with ours. */ if (mutex_lock_interruptible(&shortp_out_mutex)) return -ERESTARTSYS; /* * Out with the data. */ while (written < count) { /* Hang out until some buffer space is available. */ space = shortp_out_space(); if (space <= 0) { if (wait_event_interruptible(shortp_out_queue, (space = shortp_out_space()) > 0)) goto out; } /* Move data into the buffer. */ if ((space + written) > count) space = count - written; if (copy_from_user((char *) shortp_out_head, buf, space)) { mutex_unlock(&shortp_out_mutex); return -EFAULT; } shortp_incr_out_bp(&shortp_out_head, space); buf += space; written += space; /* If no output is active, make it active. */ spin_lock_irqsave(&shortp_out_lock, flags); if (! shortp_output_active) shortp_start_output(); spin_unlock_irqrestore(&shortp_out_lock, flags); } out: *f_pos += written; mutex_unlock(&shortp_out_mutex); return written; } /* * The bottom-half handler. */ static void shortp_do_work(struct work_struct *work) { int written; unsigned long flags; /* Wait until the device is ready */ shortp_wait(); spin_lock_irqsave(&shortp_out_lock, flags); /* Have we written everything? */ if (shortp_out_head == shortp_out_tail) { /* empty */ shortp_output_active = 0; wake_up_interruptible(&shortp_empty_queue); del_timer(&shortp_timer); } /* Nope, write another byte */ else shortp_do_write(); /* If somebody's waiting, maybe wake them up. */ if (((PAGE_SIZE + shortp_out_tail - shortp_out_head) % PAGE_SIZE) > SP_MIN_SPACE) { wake_up_interruptible(&shortp_out_queue); } spin_unlock_irqrestore(&shortp_out_lock, flags); /* Handle the "read" side operation */ written = sprintf((char *)shortp_in_head, "%08u.%09u\n", (int)(shortp_tv.tv_sec % 100000000), (int)(shortp_tv.tv_nsec)); shortp_incr_bp(&shortp_in_head, written); wake_up_interruptible(&shortp_in_queue); /* awake any reading process */ } /* * The top-half interrupt handler. */ static irqreturn_t shortp_interrupt(int irq, void *dev_id) { if (! shortp_output_active) return IRQ_NONE; /* Remember the time, and farm off the rest to the workqueue function */ ktime_get_real_ts64(&shortp_tv); queue_work(shortp_workqueue, &shortp_work); return IRQ_HANDLED; } /* * Interrupt timeouts. Just because we got a timeout doesn't mean that * things have gone wrong, however; printers can spend an awful long time * just thinking about things. */ static void shortp_timeout(struct timer_list *unused) { unsigned long flags; unsigned char status; if (! shortp_output_active) return; spin_lock_irqsave(&shortp_out_lock, flags); status = inb(shortp_base + SP_STATUS); /* If the printer is still busy we just reset the timer */ if ((status & SP_SR_BUSY) == 0 || (status & SP_SR_ACK)) { shortp_timer.expires = jiffies + TIMEOUT; add_timer(&shortp_timer); spin_unlock_irqrestore(&shortp_out_lock, flags); return; } /* Otherwise we must have dropped an interrupt. */ spin_unlock_irqrestore(&shortp_out_lock, flags); shortp_interrupt(shortp_irq, NULL); } static struct file_operations shortp_fops = { .read = shortp_read, .write = shortp_write, .open = shortp_open, .release = shortp_release, .poll = shortp_poll, .owner = THIS_MODULE }; /* * Module initialization */ static int shortp_init(void) { int result; /* * first, sort out the base/shortp_base ambiguity: we'd better * use shortp_base in the code, for clarity, but allow setting * just "base" at load time. Same for "irq". */ shortp_base = base; shortp_irq = irq; shortp_delay = delay; /* Get our needed resources. */ if (! request_region(shortp_base, SHORTP_NR_PORTS, "shortprint")) { printk(KERN_INFO "shortprint: can't get I/O port address 0x%lx\n", shortp_base); return -ENODEV; } /* Register the device */ result = register_chrdev(major, "shortprint", &shortp_fops); if (result < 0) { printk(KERN_INFO "shortp: can't get major number\n"); release_region(shortp_base, SHORTP_NR_PORTS); return result; } if (major == 0) major = result; /* dynamic */ /* Initialize the input buffer. */ shortp_in_buffer = __get_free_pages(GFP_KERNEL, 0); /* never fails */ shortp_in_head = shortp_in_tail = shortp_in_buffer; /* And the output buffer. */ shortp_out_buffer = (unsigned char *) __get_free_pages(GFP_KERNEL, 0); shortp_out_head = shortp_out_tail = shortp_out_buffer; mutex_init(&shortp_out_mutex); /* And the output info */ shortp_output_active = 0; spin_lock_init(&shortp_out_lock); timer_setup(&shortp_timer, shortp_timeout, 0); /* Set up our workqueue. */ shortp_workqueue = create_singlethread_workqueue("shortprint"); /* If no IRQ was explicitly requested, pick a default */ if (shortp_irq < 0) switch(shortp_base) { case 0x378: shortp_irq = 7; break; case 0x278: shortp_irq = 2; break; case 0x3bc: shortp_irq = 5; break; } /* Request the IRQ */ result = request_irq(shortp_irq, shortp_interrupt, 0, "shortprint", NULL); if (result) { printk(KERN_INFO "shortprint: can't get assigned irq %i\n", shortp_irq); shortp_irq = -1; shortp_cleanup (); return result; } /* Initialize the control register, turning on interrupts. */ outb(SP_CR_IRQ | SP_CR_SELECT | SP_CR_INIT, shortp_base + SP_CONTROL); return 0; } static void shortp_cleanup(void) { /* Return the IRQ if we have one */ if (shortp_irq >= 0) { outb(0x0, shortp_base + SP_CONTROL); /* disable the interrupt */ free_irq(shortp_irq, NULL); } /* All done with the device */ unregister_chrdev(major, "shortprint"); release_region(shortp_base,SHORTP_NR_PORTS); /* Don't leave any timers floating around. Note that any active output is effectively stopped by turning off the interrupt */ if (shortp_output_active) del_timer_sync (&shortp_timer); flush_workqueue(shortp_workqueue); destroy_workqueue(shortp_workqueue); if (shortp_in_buffer) free_page(shortp_in_buffer); if (shortp_out_buffer) free_page((unsigned long) shortp_out_buffer); } module_init(shortp_init); module_exit(shortp_cleanup);
这篇关于Linux设备驱动程序学习笔记——第十章 中断处理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-03-30[译]漫画SELinux概念
- 2024-03-29linux 移动文件
- 2024-03-28linux .so file
- 2024-03-28Linux 磁盘管理
- 2024-03-28Linux学习笔记(十三)磁盘管理(一):磁盘分区
- 2024-03-26linux 创建 文件
- 2024-03-25使用SecureCRT对Linux vim进行颜色设置
- 2024-03-202019-2020-12 20199317 《Linux内核原理与分析》 第十二周作业
- 2024-03-20Linux运维的第二周总结
- 2024-03-13how to count number of directories in linux