分类 linux 下的文章

在Linux系统中,抢占式调度是通过内核的调度器来实现的,它在内核中的实现是一种基于时间片轮转(Round-Robin)和优先级的调度策略。Linux 2.6内核版本之后,引入了完全抢占式内核模型,这意味着即使在内核中运行的代码,也可以在各种情况下被中断或抢占。以下是Linux如何进行抢占式调度的大体步骤:

1.在Linux系统中,每个进程都有一个优先级,通常从0到139。进程的优先级可以通过nice或renice命令来调整。

2.Linux内核具有多个运行队列,每个队列都包含具有相同优先级的进程。这可以确保高优先级进程拥有更好的CPU运行时间,并且不会一直阻塞在等待队列中。

3.内核启动后,将由特殊进程init启动的第一个进程(通常是/bin/bash)添加到运行队列中。

4.运行队列被周期性地遍历,内核为每个队列中的进程分配一个时间片。

5.当进程的时间片耗尽时,内核将终止该进程的运行,并将其从当前队列中删除。

6.如果在一个队列中运行的进程需要等待I/O或其他资源(例如磁盘、网络等),那么该进程将被放入相应的等待队列中。当等待的资源变为可用时,内核将重新将该进程添加到适当的运行队列中。

7.如果有挂起的高优先级进程,内核通过抢占机制将该进程插入到当前进程的时间片中,并为其提供CPU时间片来运行。此时,内核将挂起原来运行的进程,并继续运行更高优先级的进程。

通过上述步骤,Linux内核实现了一种抢占式调度的机制,它可以在实时响应和非实时任务之间找到平衡点,并按照优先级给予适当的CPU时间。

锁机制

在抢占式调度中,为了确保多个进程之间的协调运行,通常需要对一些共享资源进行加锁。锁的机制可以防止出现一些并发问题,比如竞争条件、死锁等等,保护进程的完整性。

在Linux内核中,抢占式调度使用了自旋锁和读写锁(spinlock和rwlock)来确保临界区的互斥,而不会出现睡眠锁(sleep lock)的情况,以免影响响应时间。自旋锁允许一个持有锁的进程能够自旋在一个循环中,等待其他进程放弃锁,而不是进入等待队列或挂起状态。自旋锁不能用于保护长时间运行的临界区,否则会导致缺乏可调度性(优先级反转问题,优先级比自旋锁持有者高的进程被阻塞在自旋锁上)。而读写锁则允许许多进程同时读取资源,但只允许一个进程对资源进行写入操作。

因此,在实现抢占式调度时,锁的使用是必须的。在Linux内核中,锁机制被集成在调度器中,用于保护多个进程之间的协调运行,以确保实时响应任务的正确性和及时性。

进程

进程是系统分配资源和调度的基本单位。一个应用程序为1个进程。地址独立。

内存管理

进程在内存主要分为5个区:
1.代码区 (.text) - 存放函数体的二进制代码
2.文字常量区 (.rodata) - 存放常量字符串
3.静态区 (static) - 存放全局变量、静态变量
4.堆区 (heap) - 开发者手动分配的内存空间,结构类似链表
5.栈去 (stack) - 存放函数参数、局部变量。由编译器自动管理,结构类似栈。

线程

线程是cpu调度的最小单位,一个进程内的线程间资源共享。
线程是由系统内核提供的服务,用户通过系统调用让内核启动线程,内核负责线程的调用和切换。

协程

协程是go自己管理的线程,比系统线程开销更少,速度更快。

fork原理:写保护中断与写时复制

父进程和子进程不仅可以访问共有的变量,还可以各自修改这个变量,并且这个修改对方都看不见。这其实是 fork 的一种写时复制机制,这一点我们在第 5 节课中模糊提到过,而里面起关键作用的就是写保护中断。下面我们来看看这到底是怎么一回事。

实际上,操作系统为每个进程提供了一个进程管理的结构,在偏理论的书籍里一般会称它为进程控制块(Process Control Block,PCB)。具体到 Linux 系统上,PCB 就是 task_struct 这个结构体。它里面记录了进程的页表基址,打开文件列表、信号、时间片、调度参数和线性空间已经分配的内存区域等等数据

其中,描述线性空间已分配的内存区域的结构对于内存管理至关重要,我们先来看一下这个结构。在 Linux 源码中,负责这个功能的结构是 vm_area_struct,后面简称 vma。内核将每一段具有相同属性的内存区域当作一个单独的内存对象进行管理。vma 中比较重要的属性我列在下面:


struct vm_area_struct { 
  unsigned long vm_start;      // 区间首地址
  unsigned long vm_end;        // 区间尾地址
    pgprot_t      vm_page_prot;  // 访问控制权限
    unsigned long vm_flags;      // 标志位
    struct file * vm_file;       // 被映射的文件
    unsigned long vm_pgoff;      // 文件中的偏移量
  ...
}

在操作系统内核里,fork 的第一个动作是把 PCB 复制一份,但类似于物理页等进程资源不会被复制。这样的话,父进程与子进程的代码段、数据段、堆和栈都是相同的,这是因为它们拥有相同的页表,自然也有相同的虚拟空间布局和对物理内存的映射。如果父进程在 fork 子进程之前创建了一个变量,打开了一个文件,那么父子进程都能看到这个变量和文件。

fork 的第二个动作是复制页表和 PCB 中的 vma 数组,并把所有当前正常状态的数据段、堆和栈空间的虚拟内存页,设置为不可写,然后把已经映射的物理页面的引用计数加 1。这一步只需要复制页表和修改 PTE 中的写权限位可以了,并不会真的为子进程的所有内存空间分配物理页面,修改映射,所以它的效率是非常高的。这时,父子进程的页表的情况如下图所示:
父子进程页表情况图.png

在上图中,物理页括号中的数字代表该页被多少个进程所引用。Linux 中用于管理物理页面,和维护物理页的引用计数的结构是 mem_map 和 page struct。

这两个动作执行完后,fork 调用就结束了。此时,由于有父进程和子进程两个 PCB,操作系统就会把两个进程都加入到调度队列中。当父进程得到执行,它的 IP 寄存器还是指向 fork 调用中,所以它会从这个调用中返回,只不过返回值是子进程的 PID。当子进程得到执行时,它的 IP 寄存器也是停在 fork 调用中,它从这个调用中返回,其返回值是 0。

接下来,就是写保护中断要发挥作用的地方了。不管是父进程还是子进程,它们接下来都有可能发生写操作,但我们知道在 fork 的第二步操作中,已经将所有原来可写的地方都变成不可写了,所以这时必然会发生写保护中断。

我们刚才说,Linux 系统的页中断的入口地址是 do_page_fault,在这个函数里,它会继续判断中断的类型。由于发生中断的虚拟地址在 vma 中是可写的,在 PTE 中却是只读的,可以断定这是一次写保护中断。这时候,内核就会转而调用 do_wp_page 来处理这次中断,wp 是 write protection 的缩写。

在 do_wp_page 中,系统会首先判断发生中断的虚拟地址所对应的物理地址的引用计数,如果大于 1,就说明现在存在多个进程共享这一块物理页面,那么它就需要为发生中断的进程再分配一个物理页面,把老的页面内容拷贝进这个新的物理页,最后把发生中断的虚拟地址映射到新的物理页。这就完成了一次写时复制 (Copy On Write, COW)。具体过程如下图所示:
写时复制过程.png
在上图中,当子进程发生写保护中断后,系统就会为它分配新的物理页,然后复制页面,再修改页表映射。这时老的物理页的引用计数就变为 1,同时子进程中的 PTE 的权限也从只读变为读写。

当父进程再访问到这个地址时,也会触发一次写保护中断,这时系统发现物理页的引用计数为 1,那就只要把父进程 PTE 中的权限,简单地从只读变为读写就可以了。这个过程比较简单,我就不画图了,你可以自己思考一下。

当父进程再访问到这个地址时,也会触发一次写保护中断,这时系统发现物理页的引用计数为 1,那就只要把父进程 PTE 中的权限,简单地从只读变为读写就可以了。这个过程比较简单,我就不画图了,你可以自己思考一下。

glibc 对系统调用的封装

我们以最常用的系统调用 open,打开一个文件为线索,看看系统调用是怎么实现的。这一节我们仅仅会解析到从 glibc 如何调用到内核的 open,至于 open 怎么实现,怎么打开一个文件,留到文件系统那一节讲。
现在我们就开始在用户态进程里面调用 open 函数。
为了方便,大部分用户会选择使用中介,也就是说,调用的是 glibc 里面的 open 函数。这个函数是如何定义的呢?
int open(const char *pathname, int flags, mode_t mode)
在 glibc 的源代码中,有个文件 syscalls.list,里面列着所有 glibc 的函数对应的系统调用,就像下面这个样子:

# File name Caller Syscall name Args Strong name Weak names
open - open Ci:siv __libc_open __open open

另外,glibc 还有一个脚本 make-syscall.sh,可以根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件。这个文件里面定义了一些宏,例如 #define SYSCALL_NAME open。
glibc 还有一个文件 syscall-template.S,使用上面这个宏,定义了这个系统调用的调用方式。

T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
ret
T_PSEUDO_END (SYSCALL_SYMBOL)
#define T_PSEUDO(SYMBOL, NAME, N) PSEUDO (SYMBOL, NAME, N)

这里的 PSEUDO 也是一个宏,它的定义如下:

#define PSEUDO(name, syscall_name, args) \
.text; \
ENTRY (name) \
DO_CALL (syscall_name, args); \
cmpl $-4095, %eax; \
jae SYSCALL_ERROR_LABEL

里面对于任何一个系统调用,会调用 DO_CALL。这也是一个宏,这个宏 32 位和 64 位的定义是不一样的。






- 阅读剩余部分 -

一、查看磁盘使用情况

[rd@mark-k8s-log-213-235 mark]$ df -lh
文件系统        容量  已用  可用 已用% 挂载点
devtmpfs        7.8G     0  7.8G    0% /dev
tmpfs           7.8G   24K  7.8G    1% /dev/shm
tmpfs           7.8G  928K  7.8G    1% /run
tmpfs           7.8G     0  7.8G    0% /sys/fs/cgroup
/dev/vda1        50G  7.5G   40G   16% /
/dev/vdb1       394G  309G   86G   79% /data
tmpfs           1.6G     0  1.6G    0% /run/user/0
tmpfs           1.6G     0  1.6G    0% /run/user/2000

二、查看目录大小

[rd@mark-k8s-log-213-235 mark]$ du -sh *
3.3M    fe-mk-admin-mis
396K    fe-mk-h5-student
604K    fe-mk-h5-teacher
452K    fe-mk-rd-toolmis
1.3M    fe-mk-resource-mis
21M    fe-mk-teacher-mis
34G    markapi
43G    mkexam
108M    mk-node-export-server
7.3M    mkresource
113G    mkscanner
4.0G    mksmartpen
106M    mktag
50G    mktiku
75G    mktool
18M    rewrite

等同于

[rd@mark-k8s-log-213-235 mark]$ du -h --max-depth=1 
20M    ./fe-mk-teacher-mis
592K    ./fe-mk-h5-teacher
106M    ./mk-node-export-server
1.3M    ./fe-mk-resource-mis
72G    ./mktool
3.2M    ./fe-mk-admin-mis
7.4M    ./mkresource
106M    ./mktag
380K    ./fe-mk-h5-student
113G    ./mkscanner
49G    ./mktiku
18M    ./rewrite
33G    ./markapi
4.0G    ./mksmartpen
448K    ./fe-mk-rd-toolmis
41G    ./mkexam
309G    .

ps -ef | grep stress | grep -v grep | cut -c 9-15 | xargs kill -9

匹配关键字kill

查找字符串,在某个目录下

grep -r '' .
find . | xargs grep -r ''

xargs 命令

用来给其他命令传递参数,因为有些命令不支持管道来传递参数。
xargs

统计每个文件的行数,并按行数排序

find app/wdsapi/ -type f -name "*.php" -print0 | xargs -0 wc -l | sort -nr

不带参数时,发出get请求

-A

-A参数指定客户端的用户代理标头,即User-Agent。curl 的默认用户代理字符串是curl/[version]。

$ curl -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36' https://google.com

上面命令将User-Agent改成 Chrome 浏览器。

$ curl -A '' https://google.com

上面命令会移除User-Agent标头。

也可以通过-H参数直接指定标头,更改User-Agent。

$ curl -H 'User-Agent: php/1.0' https://google.com

- 阅读剩余部分 -

进程通信的应用场景

数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程通信方式

  1. 管道(pipe)

    • 普通管道:单工,只能单向传输;只能在家族进程间使用,由父进程创建。
    • 流管道s_pipe:半双工,可以双向传输;只能在家族进程间使用。
    • 命名管道name_pipe:无限制。
  2. 信号量(semophore)

    • 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某个进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  3. 消息队列(message queue)

    • 消息队列是消息的链表,存在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限制等缺点。
  4. 信号(signal)

    • 信号用于通知接收进程某个事件已经发生。
  5. 共享内存(shared momory)

    • 共享内存是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的ipc方式,与信号量配合使用,实现进程间的同步与通信。
  6. 套接字(socket)

    • 唯一可跨机器的ipc方式。

各通信方式的原理及实现

管道

是内核管理的一个缓冲区,连接进程的输入和输出,linux下为4k,环形结构,以便循环利用。当管道中没有信息的话,读取进程会等待,直到有数据放入;当管道放满信息的时候,放入信息的进程会等待,直到另一端进程取出信息。

细节

linux中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和vfs的索引节点inode。
通过将两个file结构指向同一个临时的vfs索引节点,而这个vfs索引节点又指向一个物理页面而实现的。

读写操作

管道实现的源代码再fs/pipe.c中,在pipe.c中有很多函数,重要的是管道读pipe_read()和管道写pipe_write().
管道写通过将字节复制到vfs索引节点指向的物理内存而写入数据,而管道读则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁,等待队列和信号。

当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的file结构。
file结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。
写入函数在内存中写入数据之前,必须检查vfs索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:
内存中有足够的空间可容纳所有要写入的数据;
内存没有被读程序锁定。

没有实际排查过,找了一篇网上的文章。
https://www.cnblogs.com/grey-wolf/p/10936657.html

  1. close_wait 危害
  2. 问题分析
  3. 解决方案

危害

问题分析

为什么会出现close_wait?
如果服务端处理时间很长,客户端等不到服务端返回,超时了,主动断开连接,发FIN,服务端回复ACK后进入close_wait

解决方案

  1. 减少接口耗时
  2. 收到FIN后,主动发FIN
    反正是服务端的问题。