linux问题

Linux目录

字符串从内存写入到磁盘的过程中到底发生了什么(一)

字符串从内存写入到磁盘

这段代码的作用就是往一个data文件中写入Hello, World!。我们就以这段C代码和Linux系统(内核版本4.X)为例子来讲解

#include <stdio.h>

int main() {
    FILE* f = fopen("data", "w+");
    fputs("Hello, World!", f);
    fclose(f);
}

API到系统调用

编写应用程序一般是调用API(应用编程接口)来完成。虽然API这个词已经被滥用了,但最初API的思想在于封装系统调用(System Call),让系统调用对用户透明。因为系统调用既然叫系统调用,就说明是和操作系统相关的。也就说,使用A系统的系统调用的程序,到了B系统可能就无法编译了。

然而归根结底,当你的应用程序需要使用到操作系统功能时候,无论调用什么API,最后还是需要系统调用。当然,写磁盘这件事是由操作系统来管理的,所以操作系统也提供了一组对应的系统调用。

以文中的例子来说,应用程序调用了API fputs,接着fputs会调用系统调用write来交由操作系统来完成接下来的工作。

顺便一提,fopen调用的open系统调用,而fclose调用的是close系统调用。

系统调用到文件系统

Linux系统为了兼容多种操作系统,把各种文件系统抽象成VFS(Virtual File System, 虚拟文件系统),VFS会提供一组未实现的接口,然后不同的文件系统提供不同的实现。比如我们之前提到的write系统调用,会调用VFS的sys_write接口,然后会调用具体文件系统的对应的write方法。

文件系统

一段字符串到了文件系统就比较复杂了,因为不仅仅要记录字符串本身,还需要记录一些元信息(属于哪个文件,在哪个目录等等)。这里以最流行的EXT4文件系统为例来进行讲解。

首先一块物理磁盘可以分多个区,每个区对应一个文件系统。EXT4文件系统中又分了多个group,每个group中有:

super block:记录inode和data block的总量和使用量等信息
inode:存放文件的元信息以及文件内容所在data block位置
data block:存放文件内容,一个文件可能会使用多个data block

那么以本文中例子来说,就是在当前目录创建了一个文件名为data的文件。对应的磁盘操作有添加一个inode用于记录该data文件,然后把该inode关联到其所在的文件夹中,这样就可以通过该文件夹找到该文件了。接着data文件对应inode中会记录一个data block,在写入完成后,该data block的内容为Hello, World。

不过,文件系统也只是让一个简单的写入,变成了一系列的磁盘写入,但是不管是inode也好,data block也好,说到底还是存储在硬盘,其本质还是往磁盘写入一些数据。

Block I/O
文件系统又是调用什么接口写磁盘的呢?在Linux中就是通过Block I/O来完成的。

在Linux中,提供了Block I/O层,用于封装对Block设备操作。Block设备就是能够随机读写一个固定块数据的设备,比如我们熟悉的HDD硬盘,HDD硬盘的一个扇区(Sector)就是其最小读写单位。一般HDD磁盘的最小读写单位(也就是扇区)为512字节。也就是说,在HDD磁盘上,写入10字节和写入512字节都是对应一次Block I/O。

文件系统会把需要待写入的数据转化为Block I/O调用。由于Block设备一次写入和寻址是比耗时的,所以Block I/O并不会立即写入,而是会缓存在队列中,进行一轮合并和重排之后再进行操作,达到优化性能的目的。

最后,Block I/O通过调用硬件设备的驱动程序,就完成了整个写入的过程。

总结
我们简单地讨论了一次写入的大体流程,还有诸多细节留待后续讲解,这里有个图片linuxI/O栈的全景

所以一个简单的文件写入接口,后面居然还有这么多事情。然而这一切,就在你一眨眼的瞬间就完成了。

网络编程 write 阻塞和非阻塞下的区别

函数write 只是将用户进程中的数据拷贝到内核缓冲区中,拷贝数据的大小取决于内核缓冲区的大小与nbytes。
阻塞下
当内核缓冲区大小不足以容纳需要写入的 nbytes,write一直阻塞等待,直到缓冲区大小 >= nbytes 时,write一次性copy,等待的过程中,有可能会被信号中断。
当内核缓冲区大小能够容纳需要写入的nbytes,write copy到缓冲区,并返回 nbytes。
非阻塞
坚持的原则是有多少写多少,如果内核缓冲区空间足够,同阻塞情况,一次性copy;如果缓冲区不够,则缓冲区的大小是有多少就写入多少,并返回该值,显然返回值小于nbytes,此种情况需要循环的方式将nbytes数据写到缓冲区;如果缓冲区是满的,则返回-1,并置error值为EAGAIN 或 EWOULDBLOCK。

https://blog.csdn.net/u010765526/article/details/89424103

select、poll、epoll之间的区别

https://www.cnblogs.com/aspirant/p/9166944.html
select:

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

  一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

   当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd


select、poll、epoll 区别总结:

1、支持一个进程所能打开的最大连接数

select: 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll: poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll: 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、FD剧增后带来的IO效率问题

select: 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll: 同上

epoll: 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

select: 内核需要将消息传递到用户空间,都需要内核拷贝动作

poll: 同上

epoll: epoll通过内核和用户空间共享一块内存来实现的。

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示:

epoll原理详解及epoll反应堆模型

Epoll原理详解
网络高并发服务器之epoll接口、epoll反应堆模型详解及代码实现
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示:

struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
  也就是这个epoll监控的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
  struct list_head rdllist;
  ...
};

我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。

在epoll中对于每一个事件都会建立一个epitem结构体,如下所示:

struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。

当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。

epoll为啥这么高效?

1、新建epoll描述符 == epoll_create()
epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。
epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。 这个文件描述符使用如下epoll_create函数来创建
2、添加或者删除所有待监控的连接 == epoll_ctl()
epoll_ctl函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的,红黑树本身插入和删除性能比较好,时间复杂度O(logN)
3、返回的活跃连接 == epoll_wait( epoll描述符 )
epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件
epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表

epoll的两种工作模式LT ET
LT(level triggered)
LT模式,也叫做水平触发模式。在该模式下,当有事件发生并调用epoll_wait后,若未及时处理,下一次调用epoll_wait仍会继续通知。
内部实现方法:将从rdlist上取出的事件重新放回去,再次调用epoll_wait仍会继续通知,直到用户处理完成主动关闭fd。
在该模式下有两种工作方式:阻塞和非阻塞。
自认为这两种工作方式并没有很明显的区别,因为只要被唤醒就一定有事件可读或者可写,阻塞模式下,一般情况并不会进入阻塞状态。在非阻塞模式下,会使用循环的方式进行读/写,直到完成或出现异常循环退出。

ET(edge trigger)
ET模式,也叫边缘触发模式,其与水平模式的区别就是,调用epoll_wait通知过的事件,不论是否经过处理,再次调用epoll_wait不会再次通知了,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此ET模式效率比LT模式高。
内部实现方法:调用epoll_wait从rdlist取出事件后就不会再放回。

epoll的ET模式为什么一定要使用非阻塞IO

ET模式下每次write或read需要循环write或read直到返回EAGAIN错误。以读操作为例,这是因为ET模式只在socket描述符状态发生变化时才触发事件,如果不一次把socket内核缓冲区的数据读完,会导致socket内核缓冲区中即使还有一部分数据,该socket的可读事件也不会被触发
根据上面的讨论,若ET模式下使用阻塞IO,则程序一定会阻塞在最后一次write或read操作,因此说ET模式下一定要使用非阻塞IO

SIGPIPE信号导致服务器进程退出的原理及解决办法

https://blog.csdn.net/qq_36359022/article/details/78851178
引言:在采用TCP协议进行文件流传输时,客户端的不正常退出导致的服务器进程直接退出。而一个稳健的高并发服务器上这样的情形是不被允许的,接下来将剖析其产生服务器进程退出的根本原因;

原因概述

过程:
当客户端与服务器保持一个长连接,并且正在由服务器端传输文件流到客户端的过程中,客户端由于某些人为或其他原因导致的客户端的套接字进程突然断开连接(进程被kill);此时服务器仍然向客户端的套接字写入数据,接着服务器进程停止;
分析:
客户端进程突然断开(进程被kill掉)时,被kill的进程在终止处理的工作中关闭所有该进程打开的描述符,这导致了一个FIN被发送给了服务器,服务器接收以后回复一个ACK;至此,该过程完成了四次挥手的前半个部分;然而此时在服务器上与客户端保持连接的该进程正在将文件流发往客户端,服务器上的该进程还并未调用close或shutdown函数,因此四次挥手的后一部分尚未完成;
根据上述,由于四次挥手没有完成后一部分,服务器仍然能向客户端发送数据,第一次数据到客户端后,客户端会响应一个RST,服务器接收后仍在向缓冲区发送数据,此时,内核向该进程发送一个SIGPIPE信号;(当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号);
注意:SIGPIPE信号的默认行为是终止进程,因此服务器进程退出;
在这里插入图片描述

解决方案

1.在服务器进程中设置忽略SIGPIPE信号,使其接收到但不退出;(不论该进程是捕获该信号还是从其信号处理函数中返回,还是忽略该信号,写操作都会返回EPIPE错误)

#include <signal.h>

void
main()
{
  /*注册信号*/
  signal(SIGPIPE, SIG_IGN);  //SIG_IGN,系统函数,忽略信号的处理程序
}

2.(自定义函数)既然该退出是由于对方进程退出导致的,那么服务器在每次准备写入缓存区的时候检查一下连接是否存在;

//判断是否断开
if (isConnection(fd) == 0)  //0连接 -1断开
   send();  //发送数据
-------------------------------------------------   
int 
isConnection(int fd)
{
   struct tcp_info info; 
   int len=sizeof(info); 
   getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len); 
   if((info.tcpi_state == TCP_ESTABLISHED))
      return 0;
   else
      return -1;
}

shutdown() 与 close()函数详解

https://www.jianshu.com/p/eecab8d50697
close()函数

#include<unistd.h>
int close(int sockfd);     //返回成功为0,出错为-1.

close 一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数,然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。

在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程

2.shutdown()函数

#include<sys/socket.h>
int shutdown(int sockfd,int howto);  //返回成功为0,出错为-1.
该函数的行为依赖于howto的值

    1.SHUT_RD:值为0,关闭连接的读这一半。

    2.SHUT_WR:值为1,关闭连接的写这一半。

    3.SHUT_RDWR:值为2,连接的读和写都关闭。

    终止网络连接的通用方法是调用close函数。但使用shutdown能更好的控制断连过程(使用第二个参数)。

shutdown()函数可以选择关闭全双工连接的读通道或者写通道,如果两个通道同时关闭,则这个连接不能再继续通信。close()函数会同时关闭全双工连接的读写通道,除了关闭连接外,还会释放套接字占用的文件描述符。而shutdown()只会关闭连接,但是不会释放占用的文件描述符。所以即使使用了SHUT_RDWR类型调用shutdown()关闭连接,也仍然要调用close()来释放连接占用的文件描述符。

Linux信号(signal) 机制分析

https://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html

在这里插入图片描述
这里特别强调了9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。

硬中断与软中断的区别

http://www.linuxidc.com/Linux/2014-03/98013.htm

十道海量数据处理面试题与十个方法大总结

https://blog.csdn.net/v_july_v/article/details/6279498/

send、recv、sendto和recvfrom函数解析

send、recv和sendto、recvfrom,一般情况下,send、recv在TCP协议下使用,sendto、recvfrom在UDP协议下使用,也可以在TCP协议下使用,不过用的很少。
https://www.cnblogs.com/developing/articles/10974904.html

一台Linux服务器最多能支撑多少个TCP连接?

https://blog.csdn.net/sqlquan/article/details/111561959
首先排除一个误区:“TCP连接四元组是源IP地址、源端口、目的IP地址和目的端口。任意一个元素发生了改变,那么就代表的是一条完全不同的连接了。拿我的Nginx举例,它的端口是固定使用80。另外我的IP也是固定的,这样目的IP地址、目的端口都是固定的。剩下源IP地址、源端口是可变的。所以理论上我的Nginx上最多可以建立2的32次方(ip数)×2的16次方(port数)个连接。这是两百多万亿的一个大数字!!”
实际上:能处理多少并发其实和每条并发的数据处理工作量有关,如果逻辑简单的就处理的并发数量比较多
假设只建立连接不接受不发送,那么对CPU的需求就比较小,只有处理三次握手阶段需要处理小号一点点CPU的资源
“进程每打开一个文件(linux下一切皆文件,包括socket),都会消耗一定的内存资源。如果有不怀好心的人启动一个进程来无限的创建和打开新的文件,会让服务器崩溃。所以linux系统出于安全角度的考虑,在多个位置都限制了可打开的文件描述符的数量,包括系统级、用户级、进程级。这三个限制的含义和修改方式如下:”

系统级:当前系统可打开的最大数量,通过fs.file-max参数可修改

用户级:指定用户可打开的最大数量,修改/etc/security/limits.conf

进程级:单个进程可打开的最大数量,通过fs.nr_open参数可修改

孤儿进程&僵尸进程&守护进程【详细实例总结】

https://blog.csdn.net/Y1013768371/article/details/88928031

进程分配内存的两种方式–brk() 和mmap()(不设计共享内存)

https://blog.csdn.net/yusiguyuan/article/details/39496057

如何查看进程发生缺页中断的次数?

ps -o majflt,minflt -C program 命令查看。

majflt代表major fault,中文名叫大错误,minflt代表minor fault,中文名叫小错误。

这两个数值表示一个进程自启动以来所发生的缺页中断的次数。

可重入内核 & 可重入函数

https://blog.csdn.net/chj1234chj/article/details/78162443
可重入内核在ULK(深入理解linux内核)中的定义是指若干个进程可以同时在内核态下执行,也就是说多个进程可以在内核态下并发执行内核代码。在单处理器上,只能实现微观上的串行,宏观上的并行,即任意时刻,只有一个进程真正执行,其他进程处于阻塞或者等待状态。这里的可重入,是指可以多个进程进入内核,并不是重复/重新进入内核
对于linux来说,可重入内核代码包含可重入函数和非可重入函数。
可重入函数是指运行时只改变局部数据结构,不改变全局数据结构;
不可重入函数是指运行该函数时也需要改变全局数据结构。
如果有多个进程进入不可重入函数时,需要相应的锁机制(互斥锁,自旋锁)来保证同一时刻只有一个进程改变涉及到的全局数据。

可重入函数的理解其实比较麻烦,可以从以下阐述:
1.可重入是与多线程无关的,一个函数被同一个线程调用2次以上,得到的结果具有可再现性。则这个函数是可重入的。
2.可重入讲究的是结果可再现性,因此,使用全局(静态)变量的函数,再次调用其函数的结果是不可再现的,这就是前面说的为何要求该函数只修改局部变量
可重入函数,描述的是函数被多次调用但是结果具有可再现性
可重入函数条件:
1,不在函数内部使用静态或者全局数据
2,不返回静态或者全局数据,所有的数据都由函数调用者提供
3,使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
4, 如果必须访问全局数据,使用互斥锁(自旋锁)来保护
5,不调用不可重入函数
6,可重入函数必须是线程安全的
补充一点,可重入函数必定是线程安全的,但是线程安全的,不一定是可重入的。不可重入函数,函数调用结果不具有可再现性,可以通过互斥锁等机制,使之能安全的同时被多个线程调用,那么,这个不可重入函数就是转换成了线程安全。



可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。



可重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。

内存连续分配方式采用的几种算法及各自优劣

https://blog.csdn.net/u014590757/article/details/80447092


linux系统调用用户态到内核态流程

https://blog.csdn.net/agoni_xiao/article/details/79034290

https://blog.csdn.net/cs2539263027/article/details/78977054
在这里插入图片描述
程序执行系统调用大致可归结为以下几个步骤:
1、程序调用libc 库的封装函数。
2、调用软中断int 0x80 进入内核。
3、在内核中首先执行system_call 函数(首先将系统调用号(eax)和可以用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL完成),接着根据系统调用号在系统调用表中查找 到对应的系统调用服务例程。
4、执行该服务例程。
5、执行完毕后,转入ret_from_sys_call 例程,从系统调用返回



常见的进程调度算法以及linux的进程调度

https://www.cnblogs.com/cobbliu/p/5389556.html
https://www.cnblogs.com/alantu2018/p/8460451.html


操作系统页面置换算法(opt,lru,LFU,fifo,clock)实现

https://www.cnblogs.com/hujunzheng/p/4831007.html


linux 命令详解

lsof

(list open files)是一个列出当前系统打开文件的工具,在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件。所以,lsof的功能很强大。一般root用户才能执行lsof命令,普通用户可以看见/usr/sbin/lsof命令,但是普通用户执行会显示“permission denied”。因此通过lsof工具能够查看这个列表对系统监测以及排错将是很有帮助的

lsof语法格式是:
lsof [options] filename

lsof abc.txt 显示开启文件abc.txt的进程
lsof -c abc 显示abc进程现在打开的文件
lsof -c -p 1234 列出进程号为1234的进程所打开的文件
lsof -g gid 显示归属gid的进程情况
lsof +d /usr/local/ 显示目录下被进程开启的文件
lsof +D /usr/local/ 同上,但是会搜索目录下的目录,时间较长
lsof -d 4 显示使用fd为4的进程
lsof -i 用以显示符合条件的进程情况
lsof -i[46] [protocol][@hostname|hostaddr][:service|port]
  46 --> IPv4 or IPv6
  protocol --> TCP or UDP
  hostname --> Internet host name
  hostaddr --> IPv4地址
  service --> /etc/service中的 service name (可以不止一个)
  port --> 端口号 (可以不止一个)

如何kill掉某个端口的进程

https://blog.csdn.net/qq_32641813/article/details/115398642
kill -9 $(losf -t -i :port)

在Linux系统查找某文件夹中内容含有指定关键字的文件

解决方案
使用find&grep指令
find <directory> -type f | xargs grep "<strings>"
说明:

代表要查找文件的所在路径
type f代表查找普通文件
为你要查找的关键字

例子:
# find /alidata/www/shop -type f | xargs grep "用户记录"

在终端执行该代码,系统会在终端输出含有该关键字的文件

Linux 找出top10大的文件的方法

在这里插入图片描述

Linux TOP命令

top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器
https://www.cnblogs.com/xiao-xue-di/p/11056861.html


find查找命令、文件后缀名

https://blog.csdn.net/qq_38410730/article/details/81416195
find命令主要用来搜索文件

which ls
查询ls命令所在路径

±符号里 -表示数值内,相当于小于;+表示数值外,相当于大于;

find涉及格式如下:

-name “文件名”
-type [文件格式]
-size [±符号][文件大小]
-ctime [±符号][数值]
-mtime [±符号][数值]
-atime [±符号][数值]
-mmin [±符号][数值]
-inum [数值]


linux下的程序调试方法汇总

https://blog.csdn.net/guochaoxxl/article/details/51878051
基本调试,获得关键变量 - print 语句
获取有关文件系统支持,可用内存,CPU,运行程序的内核状态等信息 - 查询 /proc 文件系统
最初的问题诊断,系统调用或库调用的相关问题,了解程序流程 – strace / ltrace
应用程序内存空间的问题 – valgrind
检查应用程序运行时的行为,分析应用程序崩溃 – gdb


内存检查工具的了解

linux可以使用开源的Valgrind工具包,包含多个工具:Memcheck常用语检测malloc和new这类的问题,callgrind用来检查函数调用,cachegrind用来检查缓存使用,helgrind多线程程序中的竞争。除了valgrind还可以用mtrace这些工具

Linux根据文件路径查找索引节点

根据文件路径查找索引节点
**操作系统的文件管理系统的主要作用就是,当用户需要访问一个文件时,系统可以通过用户给出的文件路径找到文件的索引节点,从而找到文件,并以文件对象的实例交付给用户进程。**下面就以系统调用open()为例来说明文件的查找过程,以加深对文件系统的理解和认识。

系统调用open()的内核函数为sys_open(),下图描述了用户进程调用系统调用open()的整个流程:
在这里插入图片描述
sys_open()系统调用打开或创建一个文件,成功后,返回该文件的文件描述符。下图是sys_open()实现代码中主要的函数调用关系图:
在这里插入图片描述
sys_open()
从sys_open()的函数调用关系图可以看出,sys_open()在做了一些简单的参数检验后,就接着调用do_sys_open(),在该函数中:

get_unused_fd()得到一个可用的文件描述符。通过该函数,可知文件描述符实质上是进程打开文件列表中对应某个文件对象的索引值;
do_filp_open()打开文件,返回一个file对象,代表由该进程打开的一个文件。进程通过这样的一个数据结构对物理文件进行读写操作;
fd_install()建立文件描述符与file对象的联系,以后进程对文件的读写都是通过操作该文件描述符而进行的。



3个级别的文件描述符表

在这里插入图片描述

  • 进程级文件描述符表(file descriptor table)

系统为每个进程维护一份文件描述符表,该表的每一个条目都记录了单个文件描述符的相关信息,包括:
控制标志(flags),目前内核仅定义了一个,即close-on-exec
打开文件描述体指针

  • 系统级打开文件表(open file table)

内核对所有打开的文件维护一个系统级别的打开文件描述表(open file description table)。表中的条目称为打开文件描述体(open file description),存储了与一个打开的文件相关的全部信息,包括:
1、文件偏移量(current file offset),调用read()和write()更新,调用lseek()直接修改
2、访问模式(file status flags),由open()调用设置,例如:只读、只写或读写等
3、i-node对象指针(v-node ptr),指向一个inode元素,从而关联物理文件

  • 文件系统i-node表(i-node table)

就像进程用pid来描述和定位一样,在linux系统中,文件使用inode号来描述,inode存储了文件的很多元信息。
每个文件系统会为存储于其上的所有文件(包括目录)维护一个i-node表,单个i-node包含以下信息:

  1. 文件类型(file type),可以是常规文件、目录、套接字或FIFO
  2. 文件的字节数
  3. 文件拥有者的User ID 文件的GroupID
  4. 文件的读、写、执行权限
  5. 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间
  6. 链接数,即有多少文件名指向这个inode
  7. 文件数据block的位置

3个数据结构对应关系

1、应用程序进程拿到的文件描述符ID ,等于拿到 进程文件描述符表的索引;
2、通过索引拿到文件指针,指向系统级文件描述符表的文件偏移量;
3、再通过文件偏移量找到inode指针,最终对应到真实的文件
在这里插入图片描述

socket 和 文件描述符之间的关系

https://blog.csdn.net/JMW1407/article/details/107631177
套接字也是文件。具体数据传输流程如下:

当server端监听到有连接时,应用程序会请求内核创建Socket;
Socket创建好后会返回一个文件描述符给应用程序;
当有数据包过来网卡时,内核会通过数据包的源端口,源ip,目的端口等在内核维护的一个ipcb双向链表中找到对应的Socket,并将数据包赋值到该Socket的缓冲区;
应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据

注意:
操作系统针对不同的传输方式(TCP,UDP)会在内核中各自维护一个Socket双向链表,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口从对应的链表中找到其对应的Socket,并会将数据拷贝到Socket的缓冲区,等待应用程序读取。

回调函数必须要用static的原因

https://blog.csdn.net/Scarlett_OHara/article/details/93903367
https://blog.csdn.net/m0_37433111/article/details/110828851
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。

有哪些常见的IO模型?理解同步与异步、阻塞与非阻塞

https://www.cnblogs.com/loveer/p/11479249.html
https://zhuanlan.zhihu.com/p/115912936
IO分两阶段(一旦拿到数据后就变成了数据操作,不再是IO):
1.数据准备阶段
2.内核空间复制数据到用户进程缓冲区(用户空间)阶段

同步与异步(线程间调用)
同步与异步是对应于调用者与被调用者,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的

同步操作时,调用者需要等待被调用者返回结果,才会进行下一步操作

而异步则相反,调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者结果

阻塞与非阻塞(线程内调用)
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718223048431.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlbnhpbmd4aW5neGluZw==,size_16,color_FFFFFF,t_70
在这里插入图片描述
在这里插入图片描述

源码分析shared_ptr实现

https://blog.csdn.net/qq_29426201/article/details/106432856?spm=1001.2014.3001.5506
在这里插入图片描述

这里称_M_pi为管理对象,它内部的_M_ptr为托管对象,管理同一块托管对象的多个shared_ptr内部共用一个管理对象(_M_pi), 这里的多个shared_ptr可能是通过第一个shared_ptr拷贝或者移动而来, 管理对象内部有两个成员变量_M_use_count和_M_weak_count, _M_use_count表示托管对象的引用计数,控制托管对象什么时候析构和释放,大概就是有N个shared_ptr的拷贝那引用计数就是N,当引用计数为0时调用托管对象的析构函数且释放内存。_M_weak_count表示管理对象的引用计数,管理对象也是一个内存指针,这块指针是初始化第一个shared_ptr时new出来的,到最后也需要delete,所以使用_M_weak_count来控制管理对象什么时候析构,我们平时用到的weak_ptr内部其实持有的就是这个管理对象的指针,当weak_ptr拷贝时,管理对象的引用计数_M_weak_count就会增加,当_M_weak_count为0时,管理对象_M_pi就会析构且释放内存

总结:shared_ptr内部使用__shared_count中的_Sp_counted_base对象来控制托管指针,_Sp_counted_base内部有_M_use_count和_M_weak_count,_M_use_count表示托管指针的引用计数,_M_weak_count表示_Sp_counted_base的引用计数,_M_use_count为0时候释放托管指针指向的内存,_M_weak_count为0时释放_Sp_counted_base指向的内存,这里_Sp_counted_base的生命线一般不会短于shared_ptr的生命线。

进程切换和线程切换有什么区别?

进程和线程切换的区别

用户和内核线程切换的区别
当然这里的线程指的是同一个进程中的线程。要想正确回答这个问题,需要理解虚拟内存。

虚拟内存

虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存,当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,那么操作系统是如何记住这种映射关系的呢,答案就是页表。

每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。

现在我们就可以来回答这个面试题了。

进程切换和线程切换的区别

最主要的一个区别在于进程切换涉及虚拟地址空间的切换而线程不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

有的同学可能还是不太明白,为什么虚拟地址空间切换会比较耗时呢?

现在我们已经知道了进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB(translation Lookaside Buffer,我们不需要关心这个名字只需要知道TLB本质上就是一个cache,是用来加速页表查找的)。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。


clone的fork与pthread_create创建线程有何不同

https://blog.csdn.net/rock_joker/article/details/72722008

1、clone的用法和pthread_create有些相似,两者的最根本的差别在于clone是创建一个LWP,对
内核是可见的,由内核调度,而pthread_create通常只是创建一个用户线程,对核心是不可见的,由线程
库调度

2、 在linux系统中,对于clone(),调用函数时如果没有设置CLONE_FS等标志,则所创建的LWP类似于进程创建的fork();设置了诸多标志后,则类似于pthread_create()

线程分为三种类型:

内核线程、轻量级进程和用户线程。
内核线程:

它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要
和一个用户进程联系起来
。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度

并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过

态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。

轻量级进程:

轻量级进程是内核支持的用户线程,它在一个单独的进程中提供多线程控制。这些轻量级进程被单独

的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期

这种绑定都是有效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个LWP都应

该有自己的程序计数器、寄存器集合、核心栈和用户栈。

用户线程:

用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和
调度的方法
。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实

现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自

己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前

线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。


内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存器的权力。用户线程不是真正的调度

实体,内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数 来调度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会 阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞。
Linux中,每个线程都有一个task_struct,所以线程和进程可以使用同一调度器调度。其实

Linux核心中,轻量级进程和进程没有质上的差别,因为Linux中进程的概念已经被抽象成了计算状态加资

源的集合,这些资源在进程间可以共享。如果一个task独占所有的资源,则是一个HWP,如果一个task和

其它task共享部分资源,则是LWP。

clone系统调用就是一个创建轻量级进程的系统调用:

int clone(int (*fn)(void * arg), void *stack, int flags, void *arg);

其中fn是轻量级进程所执行的过程,stack是轻量级进程所使用的堆栈,flags可以是前面提到的

CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的组合。Clone和fork,vfork在实现时

都是调用核心函数do_fork。

do_fork(unsigned long clone_flag, unsigned long usp, structpt_regs);

和fork、vfork不同的是,fork时clone_flag = SIGCHLD;

vfork时clone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD;



服务器所有端口号作用详解

https://blog.csdn.net/ghevinn/article/details/8553419
开始菜单-- 运行 – netstat -an(查看端口命令)

一 、端口大全

端口可分为3大类:

1) 公认端口(Well Known Ports):从0到1023,它们紧密绑定于一些服务。通常这些端口的通讯明确表明了某种服 务的协议。例如:80端口实际上总是HTTP通讯。

2) 注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。

3) 动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。

端口:0

服务:Reserved

端口复用

端口复用详解
SO_REUSEADDR可以用在以下四种情况下。 (摘自《Unix网络编程》卷一,即UNPv1)

1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。

2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。
在有多块网卡或用IP Alias技术的机器可以测试这种情况。

3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。

4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。

需要注意的是,设置端口复用函数要在绑定之前调用,而且只要绑定到同一个端口的所有套接字都得设置复用
端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。同时,这 n 个套接字发送信息都正常,没有问题。但是,这些套接字并不是所有都能读取信息,只有最后一个套接字会正常接收数据。

tcp长连接如何实现的

应用层的心跳机制
通过应用程序自身发送心跳包去检测连接是否正常,基本方法就是在服务器端设置一个Timer事件,在一定时间段内向客户端一个心跳数据包,若在一定时间内没有收到客户端的回应,那么就会认为客户端掉线;同样客户端在一定时间段内没有收到服务器的心跳包,则判断与服务器端连接断开。

TCP的keepalive机制
其实跟心跳机制大同小异,无论是客户端还是服务器端,只要有一方开启Keepalive功能后,就会在特定的时间段内发送心跳包,对端在收到心跳包后进行回复,表示自己在线。默认的Keepalive超时时间为2小时,探测次数为5次,但超时时间可以手动设置成合理的时间段

Linux网络数据包接收处理过程

https://blog.csdn.net/Windgs_YF/article/details/118406001

首先在开始收包之前,Linux要做许多的准备工作:

创建ksoftirqd线程,为它设置好它自己的线程函数,后面指望着它来处理软中断呢

协议栈注册,linux要实现许多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数注册一下,方便包来了迅速找到对应的处理函数

网卡驱动初始化,每个驱动都有一个初始化函数,内核会让驱动也初始化一下。在这个初始化过程中,把自己的DMA准备好,把NAPI的poll函数地址告诉内核

启动网卡,分配RX,TX队列,注册中断对应的处理函数

以上是内核准备收包之前的重要工作,当上面都ready之后,就可以打开硬中断,等待数据包的到来了。

当数据到来了以后,第一个迎接它的是网卡(我去,这不是废话么):

网卡将数据帧DMA到内存的RingBuffer中,然后向CPU发起中断通知

CPU响应中断请求,调用网卡启动时注册的中断处理函数

中断处理函数几乎没干啥,就发起了软中断请求

内核线程ksoftirqd线程发现有软中断请求到来,先关闭硬中断

ksoftirqd线程开始调用驱动的poll函数收包

poll函数将收到的包送到协议栈注册的ip_rcv函数中

ip_rcv函数再讲包送到udp_rcv函数中(对于tcp包就送到tcp_rcv)

one thread one loop 思想

one loop per threadt

C++ 的浅拷贝和深拷贝(结构体)

http://blog.sina.com.cn/s/blog_59c804b60102vzcf.html
拷贝有两种:深拷贝,浅拷贝

当出现类的等号赋值时,会调用拷贝函数 在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。 但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。 所以,这时,必须采用深拷贝。 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。 简而言之,当数据成员中有指针时,必须要用深拷贝。​

​建议:

我们在定义类或者结构体,这些结构的时候,最后都重写拷贝构造函数,避免浅拷贝这类不易发现但后果严重的错误产生。​

联合索引的使用原则

在最左匹配原则中,有如下说明:

最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

出现CPU负载过高的原因

程序陷入死循环
线程死锁,相互等待,导致假死状态
现象模拟

解决步骤
top 命令查看当前系统负载信息
top -H -p pid 查看指定进程中每个线程的资源占用情况
jstack pid > ./dump.log 将指定进程中线程的堆栈信息输出到文件
利用MAT工具分析内存占用

Linux kill -9 和 kill -15 的区别

默认参数下,kill 发送SIGTERM(15)信号给进程,告诉进程,你需要被关闭,请自行停止运行并退出。
kill -9 发送SIGKILL信号给进程,告诉进程,你被终结了,请立刻退出。

理解inode

http://www.ruanyifeng.com/blog/2011/12/inode.html

动态链接库中函数的地址确定—PLT和GOT

https://blog.csdn.net/wjf1991wjf/article/details/49100893

真正动态库中函数地址的解析是第一次调用的时候做的,然后如果再次用到动态库的解析过的函数,就直接用第一次解析的结果。很自然的想法就是,一定有地方存储函数的地址,否则第一次解析出来的结果,第二次调用也没法利用。 这个存储动态库函数的地方就要GOT,Global Offset Table。 OK,我们可以想象,如果我的程序里面用到了6个动态库里面的函数,那个这个GOT里面就应该存有6个条目,每个条目里面存储着对应函数的地址。事实的确是这样

栈区和堆区内存分配区别

https://blog.csdn.net/ws1296931325/article/details/85292630

什么是操作系统的原子操作

https://blog.csdn.net/qq_24309981/article/details/102690172
原子操作是不可分割的,在执行完毕前不会被任何其它任务或事件中断:

在单线程中, 能够在单条指令中完成的操作都可以认为是 原子操作,不能在单条指令中完成的操作也都可以认为不是原子操作,因为中断可以且只能发生于指令之间;
在多线程中,不能被其它进程(线程)打断的操作就叫原子操作;

可见,无论是 ++i 还是 i++ ,都是使用三个指令来实现的:

将值从内存拷贝到寄存器;
寄存器自增;
将值从寄存器拷贝到内存;

所以,在单线程中 i++(++i) 不是原子操作;

面试的时候经常问的一道题目是i++在两个线程里边分别执行100次,能得到的最大值和最小值分别是多少?

答案:2 -200

如果在多线程中是原子操作,那得到的值一定为 200
所以,在多线程中 i++(++i)也不是原子操作;
因此,i++(++i)不是原子操作。

如何调试多线程程序

https://mp.weixin.qq.com/s/TDPlfv4V2JE1G0be4RfUIA

Shell获取路径操作(dirname $0 pwd)的实现

https://www.jb51.net/article/238477.htm

basename

打印除上层路径外的基础文件名;当文件名后存在后缀时,除去后面的后缀,如 # basename include/stdio.h .h 只会打印出 stdio

basename -s:
-s 参数后面指定要去除的后缀字符,即:# basename -s .h include/stdio.h 同 # basename include/stdio.h .h 一样只会打印出 stdio

basename -a:
-a 参数可追加执行多个文件路径,取每一个路径的基础文件名并打印

Examples:
	basename /usr/bin/sort -> “sort”
	basename include/stdio.h .h -> “stdio”
	basename -s .h include/stdio.h -> “stdio”
	basename -a any/str1 any/str2 -> “str1” followed by “str2”

dirname
去除文件名中的非目录部分,删除最后一个“\”后面的路径,显示父目录

dirname /usr/bin/ -> “/usr”
dirname dir1/str dir2/str -> “dir1” followed by “dir2”
dirname stdio.h -> “.”

组合使用参数$0
在shell中,$0 指定为命令行参数的第0个参数,即当前脚本的文件名,$1 $2 指传入脚本的第 1 第 2 个参数

dirname 和 $0:

经常看到 $(dirname $0),那么这个变量存放即:当前脚本文件的父目录,注意 $0 为脚本执行时传入的脚本路径名,如下:

dirname、$0 和 pwd:

通常我们需要把当前脚本的路径作为工作路径来执行某些相对路径文件,这时就需要获取当前被执行脚本的父目录的绝对路径了,而变量 $(cd $(dirname $0); pwd) 就是用来保存当前脚本的父目录的绝对路径的,如下图:
  在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值