【Linux】进程信号 --- 信号的产生 保存 捕捉递达-2

简介: 【Linux】进程信号 --- 信号的产生 保存 捕捉递达-2

2.4 信号被捕捉递达的完整流程(内核如何实现信号的捕捉?→ vital)


1.

信号会在内核态切换到用户态的时候被进程处理,那么进程是由于什么原因进入的内核态呢?

常见的进入内核态有两种情况。当进程调用系统调用时,由于处理器要执行内核代码,则进程运行级别一定需要切换为内核态,因为用户态权限太低,等到系统调用执行完毕,进程又会由内核态切换为用户态。另一种情况是进程切换,这种情况较为常见,当进程被轮换下去的时候,进程的上下文要进行保存,内核如果想要访问进程的上下文数据,那么进程的运行级别也必须切换为内核态,否则处理器无法拿到进程的上下文数据,进程的上下文数据也就无法保存。


2.

当进程陷入内核后,执行完系统调用或者某些任务后,内核会顺便检查进程的三张表,只要信号未被阻塞并且pending信号集中对应的比特位是1,那么就可以递达该信号,递达时的处理行为如果是默认,则当前进程运行级别为内核态,操作系统正好向进程发送信号,终止杀死该进程,如果是忽略直接惰性递达即可,将pending位图的比特位由1置0后什么处理都不做。上面所说的这两种处理行为直接以内核态的身份执行即可,递达后直接返回用户态执行剩余代码即可。


但自定义行为的递达就没有那么轻松了,首先进程以内核态的身份去执行用户层的代码是万万不可以的,因为这不安全,如果用户层的代码恶意攻击操作系统呢?此时内核态的身份还正好能执行这样的恶意攻击访问内核资源的代码,这不完蛋了吗?所以递达想要执行自定义行为,则进程运行级别必须由内核态切换为用户态,通过iret汇编指令可以切回到用户态,在执行完handler后,是不能直接回到代码的下一条运行语句处的,因为跳转是需要地址的,这个过程必须有内核参与才行,你现在处于用户态,进程的地址空间等内核信息的访问都需要内核态运行级别的进程,所以此时还需要再回到内核态,在内核态中通过进程虚拟地址的跳转找到用户态运行到哪行代码处了,然后再通过iret指令将进程运行级别由内核态转为用户态。最后进程以用户态身份继续向下执行剩余代码即可。


32297681d4bc4ade94b3d16d3eb16763.png

3.

在上面叙述的过程中,进程执行handler方法后为什么不能直接回到main执行流?而是需要先回到内核态,然后再通过某些汇编指令(iret)回到用户态,恢复main函数的上下文继续执行。

我上面的解释其实是有问题的,我从进程地址空间的角度解释了进程执行完handler方法后要回到内核态,这个角度是错误的,因为进程地址空间中的0-3G用户空间不属于内核资源.

其实真正的原因是因为,handler执行流和main执行流使用不同的堆栈空间,他们之间不存在调用和被调用的关系,是两个独立的控制流(就像fork的父进程和子进程一样,他们的堆栈空间完全独立)。所以进程是无法做到从用户层的handler执行流直接跳转到main执行流的,而是需要通过sigreturn再次进入内核态,如果此时没有信号被递达,则这次返回用户态就是恢复main函数的上下文继续执行剩余代码了。

27557fa123a5437385d4f81d379ccffa.png


4.

下面画了一张图,帮助大家理解信号捕捉递达(处理行为是自定义行为)的完整流程,从左上角开始 到 再回到左上角的一个过程。红色圈圈代表进程的运行级别要发生切换,中间的绿圈代表信号检测。(如果信号递达的行为是默认或忽略,则信号检测过后直接返回到用户态即可,无须执行handler方法)

0a9221381cc14c8fbc553d7f417fff19.png


5.

最后再总结一下信号被捕捉递达的完整流程(很详细)。


递达像是一个过程,而捕捉更像是一个动作,当信号的处理行为是自定义行为,那么在信号递达的时候会调用对应的handler方法,此时我们称调用handler方法为捕捉信号。

假设用户已经了注册某个信号(9号信号除外)的处理函数sighandler()。当前正在执行main函数,由于中断或异常切换到内核态。在中断或异常处理完毕之后,即将返回到用户态的main函数之前,内核发现用户注册的某个信号需要被递达,那此时内核决定:返回用户态不是恢复main函数的上下文继续执行,而是去执行用户注册的信号处理函数sighandler()。sighandler()执行完毕之后,由于sighandler()和main()是两个独立的控制流程,各自使用不同的堆栈空间(具体我后面在多线程部分会讲解),两者之间并不存在调用和被调用的关系。所以在sighandler()函数执行完毕,进行返回时,会自动调用特殊的系统调用sigreturn()(sys_sigreturn()是内核中该系统调用的具体实现)再次进入内核态,如果此时没有新的信号需要被递达,那么进程将会返回为用户态,内核回恢复main函数的上下文继续执行main的剩余代码。


上面的叙述过程抛出了中断和异常,以及堆栈空间等概念。


1.堆栈空间其实就是栈区,一种后进先出的数据结构,而每个函数都会有自己独立的函数栈帧空间,这些堆栈空间会被依次压入堆栈空间中,进行后进先出的处理。有很多人喜欢把栈叫做堆栈空间,堆栈空间大小是有限制的,如果函数调用层数过多,比如递归,此时堆栈空间是有可能发生stack overflow堆栈空间溢出的,所以在调用函数时要注意递归的写法,递归展开太多的话,栈溢出在所难免。


STL中的stack容器和这里的堆栈空间要区分开来,动态分配内存的容器空间一般都开辟在堆上,容器对象本身一般都是在堆栈空间上,对象中含有指向堆空间的指针。所以我们在使用stack容器时,一般是不用担心溢出的,因为堆空间很大很大有好几个G。(强调这里,是怕大家把堆栈空间和某些数据结构stack搞混掉,stack的空间一般都在堆上进行开辟,和堆栈空间是不同的)


2.时钟中断就是我们常说的进程时间片到了,进程要被轮转下去,此时需要执行内核中的中断处理程序,进程就需要被切换为内核态。软件中断,譬如在信号产生部分谈到的管道读端关闭,管道写端进程被终止。或者是alarm定时器,时间戳超过alarm定时器设定时间时,就会触发软件中断,此时进程回切换为内核态,执行内核中对应的中断处理程序。425aa94a684e4b6fb348fd97eca94962.png


下面是一个硬件中断的例子,其实可以看到,进程运行级别的切换还是非常频繁的。因为程序的运行难免要访问到内核资源,程序不是单蹦的,他也需要和操作系统进行交互,访问内核资源,所以进程运行级别切换非常频繁我们也能够理解,毕竟事物的产生和运转往往不是独立凭空出现的,而是需要配合其他事物来共同完成。

d8179190be184706b8327e07cd2f6f60.png


五、通过代码编写 理解 信号的保存和递达

1.信号集操作的库函数


1.

sigset_t类型对于所有的信号都用一个bit位来表示当前进程是否受到该信号,至于这个类型内部如何存储这些bit位,用户是不需要关心的,用户只能通过以下的库函数操作接口来操作sigset_t变量,而不应该主观的对其内部数据做任何解释。(比如你想打印printf输出sigset_t变量的值,等等操作都是不被允许的。)

前4个函数都是成功返回0,出错返回-1.sigismember是布尔函数,信号集若包含signo则返回1,不包含返回0,出错返回-1。

在使用sigset_t类型的变量之前,一定要使用sigemptyset()或sigfillset()函数对变量进行初始化,使变量内部的数据处于一个确定稳定的状态,在初始化sigset_t变量之后就可以调用剩余三个函数进行信号的添加,删除,判断是否存在等。

4a23f9486eb74eff84d6988276dc3f71.png


2.
实际上sigset_t类型是一个结构体类型的重定义,这个结构体中包含了一个类型为unsigned long int的数组,每个元素大小是8字节。至于信号是如何添加,如何删除等操作我们不关心,感兴趣的老铁可以看下源码。

ea68301d8aa54b5c8df5538435a4c37b.png


2.系统调用: sigprocmask 和 sigpending


1.

我们之前所说的block位图,其实还有一些其他的称呼:信号屏蔽字,阻塞信号集。

sigprocmask是一个可以读取或修改进程信号屏蔽字的函数,set和oset均为输出型参数,函数内部会对set和oldset指针指向的sigset_t类型变量做修改。如果oset为非空指针,则读取当前进程的信号屏蔽字通过oset指针变量传出。如果set为非空指针,则更改当前进程的信号屏蔽字,how通过传递宏的方式实现sigprocmask的不同功能,SIG_BLOCK用于添加某些信号到信号屏蔽字当中,SIG_UNBLOCK用于移除信号屏蔽字的某些信号,SIG_SETMASK用于通过set参数将函数外sigset_t类型的信号集 设置到 内核中PCB里面的信号屏蔽字。如果set和oset同时为非空指针,则先将原来的信号屏蔽字(set指向的信号集)备份到oset指向的信号集里面,然后再通过how和set参数对内核中PCB的信号屏蔽字做修改。

92486b14909846ce9bbf3a0248ca4783.png


下面便是how参数的选项,其实就是宏。

7d5d172034604ceb9d11fa8f77de1489.png


2.

sigpending用于将内核PCB中的pending位图掩码返回到set参数,进行传出。

我们可以通过这个函数取到内核中pending信号集的内容,将其放到用户层set所指向的sigset_t类型的变量里面,用户层就可以输出sigset_t信号集变量的内容,进行观察等一系列操作。8128154d0d754a2099bbcf8bbd8a96fa.png


8128154d0d754a2099bbcf8bbd8a96fa.png

3.上面所学接口的代码实现


1.
在了解上面与信号有关的库函数接口以及系统调用接口之后,我们可以来实现一段代码,我们想屏蔽一下2,3号信号,此时向进程发送对应信号,信号一定是不被递达的,但是pending位图中的第2和第3个比特位一定被置为1了,我也想看看pending位图的变化。以上现象我们通过代码运行结果来观察。

2.
这段代码在理解上有一个关键点就是用户层和内核层的分辨,在开始屏蔽数组sigarr内部的信号之前所做的工作,其实都是在用户层准备的工作,对内核中的block信号集,pending信号集未产生任何影响,第一行的signal会陷入内核,因为他要把myhandler的函数地址设置进信号处理函数的方法表里面,所以进程会陷入内核。而其他我们定义的block oblock pending等sigset_t类型的变量实际都是为使用系统调用接口做的准备工作,用一些库函数sigemptyset() sigaddset() 进行变量的初始化,做完这些准备工作之后,我们才调用系统调用接口,比如sigprocmask将用户层定义的block信号集设置进内核的信号屏蔽字当中,让进程对2和3信号进行阻塞,我们想看看在阻塞过程中,如果我们向进程发送信号,进程是否会递达呢?并且还想看到pending信号集的变化,所以需要调用sigpending系统调用接口,将内核中的pending信号集不断的加载到用户层的pending对象里面来,然后我们多次打印这个pending对象的内容即可。我们当然无法通过调用某个函数输出pending对象内容,但可以利用一下sigismember来判断所有的信号是否在pending位图中,如果是就输出1,不是就输出0,这样打印出的一行结果正好就相当于32个比特位。在10s之后,我们对信号解除阻塞,解除的方式也很简单,调用sigprocmask,将oblock的内容设置到内核即可,oblock中的比特位全部都是0,则相当于解除对所有信号的屏蔽,解除屏蔽之后,此时进程刚好处于内核态(因为调用了sigprocmask系统调用),检测到有信号需要被递达,那么直接递达该信号即可


ed2961f6c2ed4570b7f207f61c721821.png



3.

下面来看一下代码的运行结果,在代码跑起来的前10s,我利用热键向进程发送2号和3号信号,可以看到的现象是pending位图的第二个比特位和第三个比特位都被置为了1,但是在这10s内进程不会递达信号,等到10s过后,进程解除所有被屏蔽的信号,此时信号会被递达,pending位图的所有比特位又全部变成了0.

1bec7bb5274a4f7fbee9cdcc61a91e6a.gif


4.sigaction和signal的区别(代码验证)


1.

sigaction和signal的作用很相似,都可以用来进行信号的捕捉,signal使用起来较为简单,只需要传信号编号和handler函数指针即可。而sigaction从参数的命名上来看,有点像sigprocmask,两者都有当前的 和 原来的,分别通过带old和不带old进行命名。


2.

与signal相同,signum为需要处理的信号的编号,所以当调用signal或sigaction时就代表我们不想按照信号本身的默认行为进行信号递达,而是想要通过自己定义的处理行为进行信号的递达。

若act为非空指针,则根据act修改对应信号的处理行为。若oldact为非空指针,则通过oldact传出内核中对于该信号的原本的处理动作,这个就有点像sigprocmask取出内核中信号屏蔽字的过程。若oldact和act均为非空,则还是将act的处理行为备份到oldact里面,再根据act修改内核中对应信号的处理行为。


结构体struct sigaction{}的定义如下图所示(这里有点特殊哈,结构体的命名和系统调用的命名均为sigaction,老铁们不要混在一块儿),其中的三个结构体成员与普通信号无关,我们也就不用这几个成员了,只用sa_handler和sa_mask即可,前者代表信号自定义处理行为的执行方法,后者其实代表进程在处理信号时,顺便屏蔽的信号有哪些,将要屏蔽的信号添加到sa_mask即可。

2443a3eaf3dc48108a413a44db1fef98.png



sa_handler也可设置为宏SIG_DFL和SIG_TGN,这两个宏其实就是整型数字强转为函数指针类型了,设置后内核对于对应信号的处理行为则分别为默认和忽略。

ca10c6aafc984c118f7f10e1b57eb312.png


3.

sigaction实际上是要比signal更为安全可靠的,signal具有不可靠性,比如当前正在执行信号处理函数,如果此时相同信号被递达,则当前信号处理函数会被中断,转而执行新的信号处理函数,此时会新创建信号处理函数的函数栈帧,在新的信号处理函数执行完后,会恢复执行旧的信号处理函数,这个过程被称为信号处理函数的嵌套执行。如果多个相同类型信号被递达,则他们的处理顺序是不一定的,这无法确定。

而sigaction注册信号处理函数时,可以通过设置SA_RESTART标志来支持信号处理函数的可靠性。当正在执行信号处理函数时,如果相同信号被递达,系统会自动等待当前信号处理函数执行完毕后再重新调用该信号处理函数,而不是选择重新建立函数栈帧,这就保证了信号处理的可靠性。


下面代码可以帮助我们验证signal信号处理的不可靠性,但是我们其实无法通过显示器输出的数据看到这个信号处理的不可靠性,因为第二次执行handler的时候,第二个handler()函数的执行环境与第一个handler()函数的执行环境是不同的,包括函数的局部变量、参数、返回地址等信息都是不同的。所以第二个handler()函数输出的消息不会重复打印,我们也就无法通过输出信息看到内核重新开辟函数栈帧的现象了。

439680feaa634bf0843856fb10191178.png



4.

下面代码中,我们通过sigaction对2号信号进行捕捉,但同时又向结构体act的sa_mask里面设置了3号信号,这意味着在2号信号递达处理期间,如果向进程发送3号信号,信号也是会被阻塞的,无法被递达。

在信号被递达处理期间,同类型的信号会被OS自动添加到信号屏蔽字当中,当信号完成递达后,OS会自动解除对该信号的屏蔽。所以进程处理同类型信号的原则是串行的处理同类型信号,不能递归式的进行处理。


当信号处理函数调用结束后进行返回时,操作系统会自动解除对sa_mask中所有被阻塞的信号的阻塞状态。


e23b6b505c45454f9b3c775becacf253.png


下面是代码运行结果,在信号处理期间,我们发送2号或3号信号,他们是不会被递达的,只有递达完当前信号后,OS解除对于3号的阻塞,此时3号被递达,进程执行3号的默认行为,终止退出进程。

45cd495165574cf69692f0ab34f9fbc7.gif


六、补充知识内容

1.可重入函数


1.

假设现在有一个全局链表,main函数调用了insert头插函数,但是当函数执行一半的时候,还没有执行完剩余代码时,此时由于硬件中断使进程陷入内核,此时恰好有信号需要被递达,进程返回用户态执行handler方法,结果handler方法内部也调用了insert头插函数,恰好链表还是全局的,那么在handler内部完成了结点的头插,此时再返回内核态,若无信号递达,将返回用户态恢复main函数的上下文,正好main的上下文执行到头插的第二行代码,我们又调整了一下head指针的指向。此时就会出现问题,我们明明调用了两次头插函数,但链表只头插了一个结点。


2.

像上面这样的例子,insert被不同的执行流调用,有可能在第一次调用还未结束时就被进行第二次调用,我们称这样的现象为重入。


insert函数访问全局链表,链表有可能因为发生重入而导致结果出现错误,我们称这样的函数为不可重入函数。


反之,如果一个函数仅仅访问局部的变量或数据,则此函数为可重入函数,因为这样的函数即使发生了重入也不会出现问题,所以我们称其为可重入函数。

其实访问局部变量不会产生问题的原因还是因为,main和handler两个执行流各自处于不同的堆栈空间,insert函数是两份,你handler内部想怎么调insert就怎么调,对我main执行流又没什么影响,你爱咋调咋调,反正你访问的是局部数据或变量,这都是属于你的,而全局的数据和变量是不太一样的,因为我们两个执行流是共享这部分内容的。

e6f45f13008945baac4544a21c5511bc.png


3.
如果一个函数满足以下条件也是不可重入函数:
a.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
b.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

2.volatile关键字(保持内存的可见性)


1.

以下代码中,正常情况下,进程收到2号信号时被handler方法捕捉,在handler方法里将quit置为1,当handler执行完毕返回的时候,while循环判断为假,进程代码执行结束,自动退出。以上叙述情况确实正常,但当gcc编译时如果开了-O3级别的优化,并且quit全局变量没有volatile修饰时,此时进程的运行结果就不尽如人意了。

ed493614a728459099a0223ff80afe8e.png


2.

当无volatile修饰quit时,即使quit已经被改为1了,但进程依旧没有退出,执行着main控制流里的while死循环代码。当有volatile修饰quit的时候,quit被改为1后,main控制流的while判断为假,代码执行完毕,进程退出。

753606868957435688299cfdcbabc73a.gif



3.

那到底是什么原因呢?其实本质上和CPU的工作原理有关。

CPU的寄存器存储的其实是临时数据,当执行完handler后,CPU会将quit=0这一数据内容写回物理内存中,因为将计算的结果写入寄存器是没有意义的,寄存器只保存临时数据,所以此时物理内存中的quit为1,寄存器中的quit为0。

当编译器编译时不开优化选项,在执行while判断部分代码时,编译器就会去物理内存找quit的值,此时进程就会正常退出。

而当开优化选项时,CPU检测到你的while循环代码上下文并未对quit做任何修改,此时为了编译代码的效率,CPU就不会去物理内存中查找quit的数据,而是直接在当前寄存器内部查找quit的数据,而寄存器保存的是当进程代码加载进来的时候的quit的刚开始的数据,这个quit恰好是0,所以while判断就一直为真,所以进程就无法退出。


加volatile修饰代表的意思就是,无论你编译器开多高级别的优化,在取数据时不要去寄存器里面拿数据,而是去内存里面拿,这样就不会出现数据二义性的问题。我们称volatile的这种作用为保持内存的可见性。


4d6fd744ffd3498ba14c14f9a24dcf29.png

3.SIGCHLD信号


1.

以前我们在处理子进程退出问题时,采用的方法就是waitpid,采用waitpid比较麻烦,很影响main控制流,比如你进行阻塞式等待,那main控制流就得停下来等你子进程退出,如果你非阻塞式等待,main中还需要进行轮询的方式一遍一遍的去检测你子进程是否退出。

但今天在我们学习信号之后,回收子进程就不用那么麻烦了!实际子进程在退出时,是会给父进程发送17号信号SIGCHLD的,那父进程注册一个SIGCHLD信号的handler不就可以了吗?这样我父进程就完全不用主动的再去等子进程了,而是当子进程退出的时候,直接给我父进程发17号信号,此时代码转到handler控制流执行方法即可,在handler里面进行子进程的等待回收。

这样就不用让父进程去主动的等子进程,而是我父进程该干嘛干嘛,等你子进程退出的时候,给父进程打个电话,告诉父进程“我死了”,你赶快来回收我吧!


2.

在handler里面进行子进程等待的时候,其实要分情况的。

假如父进程fork了大量的子进程,子进程在同一时刻都退出了,父进程收到了大量的17号信号,然后进入handler方法内部,此时单纯只进行一次的waitpid当然是不行的,因为这么多进程都退出了,你就回收一个啊?其他全变僵尸进程了你不管啊?所以在handler内部要进行while循环式的回收子进程,我们将waitpid的第一个参数设置为-1,表示等待任意的子进程退出。


那如果子进程是分批退出的呢?在这种情况下,如果将waitpid设置为阻塞式等待(第三个参数传0),就会出问题,比如handler此时正在阻塞式等待某一子进程退出,但其他子进程过了一会儿又退出了,但你父进程此时正在阻塞啊,就无法回收其他子进程,所以waitpid一定要设置为非阻塞式等待(第三个参数传WNOHANG),这样的话如果为等待到子进程退出,那么waitpid函数返回值就为0,此时handler内部的循环结束,重新回到main执行流,如果再有一批子进程想要退出,那就再进入handler即可。设置为非阻塞式等待就可以把任意时刻退出的任意子进程都能够回收了。

e1ce4de627344ccb8f9cc6e4cece9494.png


3.

实际上,除上面间接通过waitpid的方式回收僵尸进程外,还可以通过父进程调用sigaction()或者是signal()将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会向父进程发送信号。

但其实SIGCHLD的默认行为就是忽略,一般情况下,系统默认的忽略和我们手动设置的忽略,通常是没有区别的,但这里是一个例外。操作系统对我们手动设置的SIG_IGN做了特殊处理,帮我们做了回收子进程的工作。

注意:此方法对于Linux系统可用,但不保证在其他UNIX系统上也可用,比如MAC OS 或 直接本身就是UNIX操作系统。

73d737322c8f43fa9358507f4c1c3854.png
















相关文章
|
4月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
115 16
|
3月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
74 0
|
3月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
102 0
|
3月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
65 0
|
3月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
67 0
|
2月前
|
监控 Linux 网络安全
Linux命令大全:从入门到精通
日常使用的linux命令整理
622 13
|
3月前
|
Linux 网络安全 数据安全/隐私保护
使用Linux系统的mount命令挂载远程服务器的文件夹。
如此一来,你就完成了一次从你的Linux发车站到远程服务器文件夹的有趣旅行。在这个技术之旅中,你既探索了新地方,也学到了如何桥接不同系统之间的距离。
452 21
|
3月前
|
JSON 自然语言处理 Linux
linux命令—tree
tree是一款强大的Linux命令行工具,用于以树状结构递归展示目录和文件,直观呈现层级关系。支持多种功能,如过滤、排序、权限显示及格式化输出等。安装方法因系统而异常用场景包括:基础用法(显示当前或指定目录结构)、核心参数应用(如层级控制-L、隐藏文件显示-a、完整路径输出-f)以及进阶操作(如磁盘空间分析--du、结合grep过滤内容、生成JSON格式列表-J等)。此外,还可生成网站目录结构图并导出为HTML文件。注意事项:使用Tab键补全路径避免错误;超大目录建议限制遍历层数;脚本中推荐禁用统计信息以优化性能。更多详情可查阅手册mantree。
linux命令—tree
|
3月前
|
监控 Linux
Linux系统中使用df命令详解磁盘使用情况。
`df`命令是Linux系统管理员和用户监控和管理磁盘空间使用的重要工具。掌握它的基本使用方法和选项可以帮助在必要时分析和解决空间相关问题。简洁但功能丰富,`df`命令确保了用户可以快速有效地识别和管理文件系统的空间使用情况。
216 13
|
1月前
|
监控 Linux Shell
linux命令
常用 Linux 命令汇总