linux复习
1.关于进程
1.1 概念
用户角度:进程是程序的一次执行实例,也就是正在运行的程序
内核角度:操作系统分配内存和cpu资源的实体
操作系统使用内核数据结构 + 程序的代码及数据 描述进程,Linux中对应的内核数据结构就是task_struct.
task_struct当中主要包含以下信息:
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器(pc): 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
ps -aux或者ps -axj可以查看系统中的所有进程,u和j是选择显示的格式不同。
同时我们在 /proc 目录中存在一个个子目录,32452,12356这些子目录就是存储了进程运行的一些相关信息,里面存在cwd和exe两个文件
1.2 fork
fork是系统调用接口,用于创建进程。在它之后,出现两个进程,原始的数据和 fork之后的代码共享,但其返回值id在两个进程中各存储 一份,我们利用id==0来进行执行分流。
1.3 进程状态
task_struct在cpu的runqueue中排队,则为运行状态R。当然它们也可以去其他资源下面排队(阻塞)。
Z状态:子进程的pcb的退出信息未被父进程读取。僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况. 僵尸进程的退出信息被保存在task_struct中,僵尸状态一直不退出,那么PCB就一直需要进行维护,出现内存泄漏。
孤儿进程:父进程先退出,子进程会被1号进程领养
1.4 环境变量
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些变量。
常见环境变量
- PATH: 指定命令的搜索路径。
- HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。
- SHELL: 当前Shell,它的值通常是/bin/bash。
我们使用echo $PATH可以查看环境变量中系统默认路径的值,env查看所有环境变量,set查看所有环境和本地变量
进程的task_struct中有一个字段为 mm_struct *, mm_struct为进程虚拟地址空间,这个地址空间的最高地址处是内核空间,下面就是环境变量,然后是命令行参数和栈区。
环境变量是OS开辟空间所存储的带有特殊功能的变量,它会传递给子进程。
1. 我们机器启动时,电脑会自动读取根目录下面和用户目录下面存在在某位置的profile文件,这两个文件分别存放了系统环境变量和用户环境变量,这时操作系统里面就有了环境变量。而我们后面在电脑所执行的进程中,都会包含这些环境变量。
2. 当我们在windows 双击某应用程序时,这个软链接(快捷方式)会存放这个程序的具体路径,如:C:\Program Files (x86)\Google\Chrome\Application\chrome.exe,然后该程序就启动了。
但程序的启动除了在当前目录下进行查找这个可执行程序(命令),它们还会去$PATH所指定的目录下去查找该命令是否存在。 因此我们如果想在任意位置执行test.exe,可以sudo cp test.exe /usr/bin, 或者 export PATH=$PATH: /xwy/cpp/code1.
main函数有三个参数,argc,argv,envp,其中envp就是环境变量,我们还可以使用const char*= getenv("USER")获取和使用环境变量。
1.5 进程切换
进程的tastk_struct 中除了pid,进程状态,进程优先级,进程地址空间(mm_struct*),还要有struct thread_struct (软件上下文),硬件上下文(tss_struct),它保存进程的一些寄存器数据(包括PC和页表基址寄存器),它们改变了,那所取出的下一条指令的地址也改变了,同时映射到物理内存的数据也改变了!!! 在 上下文切换 中,tss_struct
主要由 CPU 自动处理,而 thread_struct则由操作系统通过软件来管理。
1.6 进程调度
runqueue,两个指针,active和expired,两个struct ,arr[0]和arr[1],它们当中有nr_active记录进程数量,有bitmap[5],记录该优先级下的队列是否存在进程,queue[140],0-100不使用,每个queue都连接一定数量task_struct.
只用40个优先级在使用,对应了nice值为[-20,19], 默认优先级为80.
1.7 进程地址空间
进程task_struct当中有一个结构体指针存储的是mm_struct的地址。
父进程创建子进程后的task_struct和mm_struct:
1.8 进程相关接口
创建进程:fork(),他会拷贝原有进程的mm_struct和页表,同时在 页表中 把原有进程的数据的权限改为已读,当有一方写入时,发生缺页中断,OS介入,发生写时拷贝。
进程终止 :exit(2),_exit(2)
exit
函数会执行一些清理操作,包括调用所有已注册的终止处理函数(如atexit
注册的函数),关闭所有打开的标准流(如stdout, stderr等),释放内存,它屏蔽了底层系统调用的不同。
_exit为系统调用
函数直接将控制权返回给父进程,并传递一个退出状态码。
而return则是将返回值给调用者,标志着一个执行流的结束,而我们在main()函数中return时候,其上层的调用者最终还是会调用exit()去终止进程。当我们在子进程中 return时,也可以返回给父进程返回值,但它不会进行清理操作。
进程等待: 进程结束后会给父进程返回一个 int,int中0-16位有意义,最后七位代表信号编号,中间八位就是进程所得到的返回值,(status>>8)&0xff即可以得到返回值。
pid_t wait(int *status); //阻塞式的随机等待一个子进程
pid_t waitpid(id,status, WNOHANG); //等待指定pid进程,可设置非阻塞的等待
进程替换:execl (path,argv,...); 执行路径,执行谁,怎么执行,以null结尾。调用失败返回-1,调用成功,原来进程后面的代码不再执行。
2.关于文件
关于操作系统对文件的管理,也是先描述,再组织,文件与进程息息相关,进程可以打开多个文件,我们一般所指的就是内存文件。
struct file_struct
进程的结构中,除了mm_struct*,thread_struct*, 还需要有file_struct*,file struct结构体中除了一些属性外,还有一个指针数组,file* fd[ ],各个打开的文件描述符就依次存放在这个数组里面。
struct file
file结构体它也有很多字段,如指向文件操作函数的指针file_op* (每种类型文件的该结构体不同,内核会根据文件类型调用不同的操作 ,如块设备、字符设备)、文件标志、文件权限,文件的读写偏移量等。 它帮我们屏蔽了不同设备(如磁盘读写,显示器显示等等)的差异,我们使用read,write,它会调用不同的底层接口进行处理,在我们用户看来都是对文件的操作。
Linux下用户与内核中的交互都是通过struct file_struct和struct file。键盘显示器都被看作虚拟文件,而其余硬件设备也会打开时也会打开一个 struct file。
如下:当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
关于网络部分 : struct socket中包含struct file*
。应用程序通过 socket()
创建一个套接字时,内核会返回一个文件描述符,这个文件描述符指向一个内核中的 struct file
结构,并且可以通过 struct file
进一步操作 struct socket,struct file有一个
file_op*,这些操作表中的函数实现了套接字的读取、写入、关闭等操作,其中的read = sock_read,通常对应于 recv()
系统调用。
网络中数据的发送:当应用程序调用套接字的 send()
或 write()
时,内核将数据交给协议栈处理。数据会根据协议(如 TCP 或 UDP)进行分段、加上适当的头信息(如 IP 地址和端口号),最终通过网卡接口发送出去,其中网卡驱动对应的inode会给数据源mac地址和目的mac地址 。
其还有一个字段 f_inode
:指向文件的 inode
结构体的指针, 每个文件、目录、符号链接等对象在文件系统中都有一个唯一的 inode,
它是在分区内唯一,inode是一个文件的属性集合. Linux 内核为套接字创建了 inode
结构。在套接字的临时 inode
中,存储的是套接字的元数据(如类型、协议、端口号等)。
struct inode
inode
结构体在 Linux 中代表了文件的元数据,它包括了文件的类型、权限、大小、时间戳、以及对应的数据块位置(采用多级索引),通过这个inode可以找到其文件的内容。
磁盘的扇区(512B)被OS当成一个个的块组,这些块组又被分成inode table和data blocks。struct inode将内存和磁盘建立了联系。
相关接口
文件重定向接口:dup2(oldfd,newfd),调用之后全部全部变成old_fd.
读写相关接口 open,close,read,write
缓冲区
我们常说的缓冲区指的是C语言(C++)缓冲区,它存放在FILE结构体中,可以将其理解为一个结构体或者数组,当存储到一定量或者到了一定时间,缓冲区的数据会刷新到OS的缓冲区中(其存在于 struct file中),后面通过inode块刷新到数据块之中。
子进程会继承父进程的缓冲区,所以有时会出现printf()在fork之前,数据却打印了两份的现象。
目录
目录也是文件,有自己的inode,它的内容是文件名和inode编号的对应,当我们打开文件时,操作系统会进行文件路径的解析,依次打开相关的目录文件,读取其中信息,直到我们打开最终的文件,就是把这个文件的 inode加载进内存,同时把file结构体放入内存,并填写对应打开进程的文件描述符表。
软硬链接
ln -s test.c link.soft, 软链接分配新的inode,其文件内容指向目标文件的路径
硬链接则是将该文件名和目标inode 的对应关系放在目录下。每个目录创建后,该目录下默认会有两个隐含文件.
和..
,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.
,所以刚创建的目录硬链接数是2,如果该目录下再创建一个子目录,则硬链接则会+1
为什么说linux下一切皆文件
1. 文件系统抽象
在 Linux 中,文件系统不仅仅包括磁盘上的文件,也包括了许多其他资源。无论是常规的磁盘文件,还是特殊设备(如硬盘、终端、网络套接字等),都通过文件系统来进行管理和访问。
设备文件:硬件设备(如硬盘、键盘、显示器、打印机等)通过设备文件进行表示,通常位于 /dev
目录下。例如,/dev/sda
表示第一个硬盘,/dev/ttyS0
表示串口设备。管道和套接字:用于进程间通信的机制,它们也被视为文件,在文件系统中以特殊文件的形式存在(如 /tmp
中的临时文件,或 /proc
中的进程信息)内存:内存使用的统计信息,主要通过 /proc/meminfo
文件提供
2.统一接口
对各种资源的管理:这些操作通常通过系统调用 read()
、write()
、open()
、close()
等来完成,而这些系统调用在内核内部对不同类型的资源进行了适配和处理。开发者只需要关心如何操作文件描述符,而不需要区分文件、设备或网络等资源的具体实现。
3. 文件描述符
在 Linux 中,打开的每个文件、设备或资源都有一个唯一的文件描述符(file descriptor)。通过文件描述符,用户可以操作不同的资源。文件描述符在进程的上下文中是唯一的,内核通过一个系统调用接口(如 open()
)来管理文件描述符的分配和回收。
3.linux内存管理
1. 虚拟内存
虚拟内存是 Linux 内核内存管理的基础。每个进程在执行时,操作系统为其分配虚拟内存空间,这使得每个进程看起来拥有独立的内存。虚拟内存通过内存映射机制将虚拟地址与物理内存地址(或磁盘上的交换空间)关联起来。它的主要特点包括:
- 地址空间隔离:每个进程有自己的虚拟地址空间,操作系统负责将其映射到实际的物理内存上。
- 页(Page):虚拟内存被划分为固定大小的块,通常为 4 KB(虽然支持更大的页大小,如 2MB 或 1GB)。每个进程的虚拟地址空间都被划分为多个页。
- 分页(Paging):当进程访问虚拟地址时,内核会使用页表来将虚拟地址映射到物理地址。如果虚拟页不在物理内存中,内核会进行页面交换(页调度)。
2. 页表
页表是虚拟内存和物理内存之间的映射表。每个进程都有一个页表,它记录了进程的虚拟地址与物理地址之间的映射关系。页表项包含了每个虚拟页的物理页框号(physical page frame number)以及访问权限等信息。
- 三级页表结构:在 64 位系统中,Linux 使用三级页表(L1, L2, L3),每一层页表的大小和结构可以根据架构的不同进行调整。
- 多级页表:使用多级页表结构(例如 4 级页表)以减少每个进程的页表大小,减少内存消耗。
3. 物理内存管理
物理内存是实际的计算机硬件内存。内核需要高效地管理这些内存资源。物理内存的管理方式主要包括:
- 内存分配:Linux 使用
buddy allocator
来管理物理内存。它通过将内存划分为不同大小的块来高效地分配和回收内存。buddy allocator
的核心思想是通过合并和分割内存块来最大化利用内存。Linux 内核将物理内存划分为多个大小为 2 的幂次的页面(通常是 4KB),并使用 Buddy Allocator 来高效地分配和回收内存页。 - 内存区域划分:Linux 将物理内存划分为不同的区域,主要包括:
- ZONE_DMA:用于低地址区的内存,通常与设备直接内存访问(DMA)相关,效率高。
- ZONE_NORMAL:普通内存区域,操作系统和用户进程通常使用的内存。
- ZONE_HIGHMEM:高端内存区域,主要用于 32 位系统,系统无法直接访问的内存区域,通常需要通过特殊机制(如页表映射)才能使用。
- Slab 分配器:用于高效分配和释放内存的小块,主要用于内核对象的分配,例如进程控制块(PCB)、文件描述符等。
4. 交换空间
当物理内存不足时,Linux 内核使用交换空间(将部分内存页面换出到磁盘上。交换空间是磁盘的一块区域,它可以是一个专门的交换分区或一个交换文件。当系统需要更多内存时,内核会选择一些不活跃的页面交换出去,腾出内存供当前进程使用。
- 交换空间的使用:当内存页被交换到磁盘时,系统会将这些页面标记为“不在内存中”,如果再次访问这些页面,内核会将它们从交换空间调回内存。
- 页面交换的策略:内核使用 LRU(Least Recently Used,最少最近使用)算法来管理交换页面。LRU 算法会优先交换那些长时间未被使用的内存页。
5. 内存映射(Memory Mapping)
内存映射是将文件或设备直接映射到进程的虚拟内存空间中,这样程序就可以直接操作这些文件或设备的数据,而不需要通过传统的读取/写入接口。内存映射主要用于:
- 共享内存:多个进程可以共享同一块物理内存,进程间通信。
- 内存映射文件:将文件映射到进程的地址空间,从而使得程序能够像访问内存一样访问文件。Linux 中使用
mmap()
系统调用来实现内存映射,程序可以直接在内存中操作文件,能显著提高读写效率。 - 映射设备:设备可以通过内存映射映射到进程的虚拟内存空间,允许直接与硬件交互。
6. 内存页面回收
当系统内存资源紧张时,Linux 内核会回收一些不再使用的内存页面。页面回收通常通过以下几种方式完成:
- 清理页面:如果内存页被修改,但尚未被写回磁盘,内核会先将这些页写回磁盘。
- 懒惰写回:内核会定期将脏页(已修改但未写回的页面)写回磁盘。
- 直接回收:对于长期未被访问的页面,内核会直接将其回收并释放内存。
7. 内存保护与访问控制
Linux 通过内存保护来确保进程之间互相隔离,防止非法访问其他进程的内存。内核使用页表项中的标志位来控制访问权限(如读、写、执行等)。常见的内存保护机制包括:
- 只读内存:防止进程修改某些特定内存区域。
- 执行保护:防止执行某些特定区域的代码(如堆栈区、数据区等)。
- 堆栈保护:为了防止堆栈溢出,内核会为堆栈设置特殊的保护页。
8. 内存使用统计
Linux 内核还提供了内存使用的统计信息,主要通过 /proc/meminfo
文件提供。这些统计信息包括:
- 总内存:系统的总物理内存大小。
- 可用内存:系统当前未被使用且可用于进程的内存。
- 缓存和缓冲区:内核为提高性能而缓存的内存区域。
- 交换空间:磁盘上用于交换的空间大小及其使用情况。
用户和管理员可以通过这些信息了解系统的内存使用情况,调整系统配置,避免内存不足等问题。
4.动静态库
1.基本原理
动静态库是可执行程序的半成品。
对于频繁使用的test1.c,test2.c,test3.c,我们可以把它打包在一起,供main1.c,main2.c调用。
一堆源文件和头文件最终变成一个可执行程序需要经历以下四个步骤:
预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。
编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。
汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件。
链接: 将生成的各个xxx.o文件进行链接,进行符号汇总解析,段合并,地址分配(生成绝对地址),代码和数据中地址的重定位并生成最终的elf可执行程序,(.text,
.data,
.bss会
分配这些段在可执行文件中的地址)
静态库采用绝对编址,会把所有的函数和变量都形成一个统一的绝对地址,链接过程中链接器根据程序中使用的符号,提取库中仅与这些符号相关的部分加载进可执行程序中。
动态库采用绝对编制和相对编制结合的方式。
- 动态库中函数和变量的符号在编译时会记录一个固定的偏移地址。
- 链接器在运行时将这些符号的引用解析为库中的绝对地址。
- 其他函数和变量的符号直接形成一个绝对地址。
如果我们使用printf函数,编译器编译时只需要记录这个函数在C标准库的偏移量(0x11223344),等到实际运行时会根据库函数实际加载的地址(0x22334455),加上之前记录的偏移量,找到该函数的具体地址。
进程地址空间实际由编译器+页表+CPU共同完成。
2.查看动静态库
ldd a.out
可以看到a.out链接的动态库,其中的libc.so.6
就是该可执行程序所依赖的库文件,我们通过ls命令可以发现libc.so.6
实际上只是一个软链接。
libc-2.17.so
实际上就是一个共享的目标文件库,准确来说,这还是一个动态库。
- 在Linux当中,以
.so
为后缀的是动态库,以.a
为后缀的是静态库。 - 在Windows当中,以
.dll
为后缀的是动态库,以.lib
为后缀的是静态库。
3.动静态库的制作
第一步:让所有源文件生成对应的目标文件
静态库:gcc -c add.c 和 gcc -c sub.c; 编译时的 -c选项就是让这个.c文件走完预处理,编译汇编三个过程。
动态库:gcc -fPIC -c add.c 和 gcc -fPIC -c sub.c;
第二步: 打包生成静态库文件
静态库:ar -rc libcal.a add.o sub.o
动态库 :gcc -shared -o libcal.a add.o sub.o
第三步:将头文件和生成的静态库组织起来
在这里我们可以将add.h
和sub.h
这两个头文件放到一个名为include的目录下,将生成的静态库文件libcal.a
放到一个名为lib的目录下,然后将这两个目录都放到mathlib下,此时就可以将mathlib给别人使用了。
使用:
方法一:使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
示例: gcc main.c -I ./mathlib/include -L./mathlib/lib -lcal
方法二:把头文件和库文件拷贝到系统路径下
然后我们使用时只需要-l
:指明需要链接库文件路径下的哪一个库即可。
除了以上这些,对于动态库,则多一个步骤,因为我们使用-I
,-L
,-l
这三个选项都是在编译生成可执行程序期间编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库在哪里,我们可以使用ldd a.out
命令进行查看。
方法一:拷贝.so文件到系统共享库路径下
既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。如:sudo cp mlib/lib/libcal.so /lib64
方法二:更改
LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xwy/BasicIO/testlib/project/mlib/lib
,LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中即可。
方法三:配置
/etc/ld.so.conf.d/
动态库执行起来之后依然需要 操作系统找得到对应动态库的位置并加载进内存。
5.进程间通信
我们讲匿名管道和命名管道两种进程间通信的方式,还有POSIX的进程(线程通信)。systemV那三种共享内存,消息队列,信号量只讲原理。
管道定义:我们把从一个进程连接到另一个进程的数据流称为一个“管道”。
例如,统计我们当前使用云服务器上的登录用户个数。 who|wc -l
who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出stdout 将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
1. 匿名管道
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现进程间通信的原理就是,让两个具有血缘关系的进程先看到同一份被打开的文件资源,然后两进程就可以对该文件进行写入或是读取操作,进而实现进程间通信。
这张图非常的重要,不仅说明了匿名管道的通信原理。还可以说明进程与文件的联系,同时不同类型的inode有不同的结构。网络套接字socket,磁盘,键盘鼠标都有它特定的inode结构和操作方法operator* op(存在file结构中),它决定最终放在文件缓冲区的内容是否刷到特定的物理设备上,以及何种物理设备(网卡,键盘,屏幕)上面。
2. 相关接口
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符,pipe函数调用成功时返回0,调用失败时返回-1。
这个系统调用执行后OS会打开一个“文件”,然后返回读写两端的文件描述符,当fork()之后,父子进程就拿到对同一个文件的两个文件描述符。与以读写方式两次open统一文件不同的是
1.它不需要指定文件名
2.父子进程任何一个进行write时它不会发生写时拷贝。
int pipe2(int pipefd[2], int flags);
额外的 flags
参数:pipe2
允许用户设置管道的标志,最常见的是 O_CLOEXEC
,可以在管道创建时设置文件描述符在 exec
系统调用时自动关闭。 pipe
创建的管道文件描述符默认没有设置 O_CLOEXEC
标志,这意味着如果后续调用 exec
时,这些文件描述符会被继承。
当使用 O_NONBLOCK
标志创建管道时,管道的读写操作变成非阻塞模式。如:管道为空,read读取会直接返回-1,错误码被设置为EAGAIN或 EWOULDBLOCK
。管道满,write返回-1,错误码被设置为EAGAIN或 EWOULDBLOCK
。
// 两种标志的使用示例
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC) == -1) {perror("pipe2");exit(EXIT_FAILURE);
}
int pipefd[2];if (pipe2(pipefd, O_NONBLOCK) == -1) {perror("pipe2");exit(EXIT_FAILURE);
}
写相关代码记住fd的关闭以及父进程的 wait。
3. 管道的特点
1、管道内部自带同步与互斥机制。管道在同一时刻只允许一个进程对其进行写入或读取
2、管道的生命周期随进程。
3、管道提供的是流式服务。不同于有明确分割的数据报服务
4、管道是半双工通信的。可以边写边读,但是读和写已经定义好了,属于半双工服务。
如果管道没有读端,或者读端没有读取数据,在阻塞模式下,管道的写操作会阻塞,直到有一个进程打开管道的读端并开始读取数据。在非阻塞模式下,写操作失败,设置 errno
为 EPIPE
4. 管道的四种特殊情况
1. 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
3.写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
讲第四种,子进程在write,父进程直接 close(fd[0]); 父进程关闭读端,子进程会收到13号信号SIGPIPE从而杀死子进程。
2. 命名管道
1. 使用命令创建命名管道
我们可以使用mkfifo
命令创建一个命名管道。
mkfifo fifo
fifo的文件类型是p,即管道文件,然后我们开cat >fifo不断读取该管道文件(cat fifo也可以,cat命令本身是读取文件内容),然后我们在另一终端执行脚本 while : ; do echo "hello"; sleep 1; done >> fifo,可以看到 读端显示hello
2. 相关接口
int mkfifo(const char *pathname, mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
如果将mode设置为0666,则命名管道文件创建出来的权限如下:rw-rw-r--,因为mask为0002,因此需要在创建文件前使用umask
函数将文件默认掩码设置为0。
用命名管道实现serve&client通信
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
共用头文件:
//comm.h
#pragma once#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
服务端的代码如下:
//server.c
#include "comm.h"int main()
{umask(0); //将文件默认掩码设置为0if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件perror("mkfifo");return 1;}int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件if (fd < 0){perror("open");return 2;}char msg[128];while (1){msg[0] = '\0'; //每次读之前将msg清空//从命名管道当中读取信息ssize_t s = read(fd, msg, sizeof(msg)-1);if (s > 0){msg[s] = '\0'; //手动设置'\0',便于输出printf("client# %s\n", msg); //输出客户端发来的信息}else if (s == 0){printf("client quit!\n");break;}else{printf("read error!\n");break;}}close(fd); //通信完毕,关闭命名管道文件return 0;
}
对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
客户端的代码如下:
//client.c
#include "comm.h"int main()
{int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件if (fd < 0){perror("open");return 1;}char msg[128];while (1){msg[0] = '\0'; //每次读之前将msg清空printf("Please Enter# "); //提示客户端输入fflush(stdout); //这里我们也可以用dup2()接口//从客户端的标准输入流读取信息ssize_t s = read(0, msg, sizeof(msg)-1);if (s > 0){msg[s - 1] = '\0';//将信息写入命名管道write(fd, msg, strlen(msg));}}close(fd); //通信完毕,关闭命名管道文件return 0;
}
命令行当中的管道(“|”)到底是匿名管道还是命名管道呢?
下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。
3. system V进程间通信
管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,system V IPC是操作系统特地设计的进程间通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。
system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
1.共享内存的基本原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。shmget函数,应该为他分配一个key,标识这个共享内存。
2. 消息队列的基本原理
消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。msgget,也需要一个key。
3.信号量
信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时需要将sem减减。信号量也需要一个key。
共享内存、消息队列以及信号量,虽然它们内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量。
这样设计的好处就是,在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。
会话与服务
建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。如果我们再xshell连接服务器终端,终端将会有一个控制进程,当我们断开连接时,服务器终端上的控制进程会收到一号信号SIGHUP而终止控制进程。
守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程,即服务
我们创建守护进程时可以直接调用daemon接口进行创建,daemon函数的函数原型如下:
int daemon(int nochdir, int noclose);
参数说明:
- 如果参数nochdir为0,则将守护进程的工作目录该为根目录,否则不做处理。
- 如果参数noclose为0,则将守护进程的标准输入、标准输出以及标准错误重定向到
/dev/null
,否则不做处理。
6.信号相关
常见信号
1 SIGHUP 如果终端接口检测到一个连接断开,则会将此信号发送给与该终端相关的控制进程,该信号的默认处理动作是终止进程。
2 SIGINT 当用户按组合键(一般采用Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号的默认处理动作是终止进程。
3 SIGQUIT 当用户按组合键(一般采用Ctrl+\)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号不仅终止前台进程组,同时会产生一个core文件。
6 SIGABRT 调用abort函数是产生此信号,进程异常终止,同时会产生一个core文件。
8 SIGFPE 此信号表示一个算术运算异常,比如除0、浮点溢出等,该信号的默认处理动作是终止进程,同时产生一个core文件。
9 SIGKILL 该信号不能被捕捉或忽略,可以杀死任一进程的可靠方法。
11 SIGSEGV 指示进程进行了一次无效的内存访问(比如访问了一个未初始化的指针),该信号的默认处理动作是终止进程并产生一个core文件。
13 SIGPIPE 如果在管道的读进程已终止时对管道进行写入操作,则会收到此信号,该信号的默认处理动作是终止进程。 要想不产生僵尸进程:父进程调用signal或sigaction函数将SIGCHLD信号的处理动作设置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
14 SIGALRM 当用alarm函数设置的定时器超时时产生此信号,或由setitimer函数设置的间隔时间已经超时时也产生会此信号。
17 SIGCHLD 在一个进程终止或停止时,SIGCHLD信号被发送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种wait函数以取得子进程ID及其终止状态。
19 SIGSTOP 这时一个作业控制信号,该信号用于停止一个进程,类似于交互停止信号(SIGTSTP),但是该信号不能被捕捉或忽略。
20 SIGTSTP 交互停止信号,当用户按组合键(一般采用Ctrl+Z)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程。
信号是进程之间事件异步通知的一种方式,属于软中断。
信号的原理
我们都知道进程控制块本质上就是一个结构体变量,而对于信号来说我们主要就是记录某种信号是否产生,因此,PCB中有 一个32位的位图来记录信号是否产生。这也就是pending位图:
还有一个block位图,以及handler注册表,它们来构成信号的阻塞,以及捕获信号时的处理方法。
信号产生的四种方法
1. 组合按键产生,CTRL+c
2.软件中断产生,管道中的13号信号。
3. 由硬件异常产生信号。硬件上面的信息也会立马被操作系统识别,有状态码寄存器
4.系统函数产生。kill(),absort,alarm,raise等函数可以产生信号,alarm函数的作用就是,让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程。
信号集的使用
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void printPending(sigset_t *pending)
{int i = 1;for (i = 1; i <= 31; i++){if (sigismember(pending, i)){printf("1 ");}else{printf("0 ");}}printf("\n");
}
int main()
{// signal(2,handler); 注册信号的自定义方法sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);sigaddset(&set, 2); //SIGINTsigprocmask(SIG_SETMASK, &set, &oset); //阻塞2号信号sigset_t pending;sigemptyset(&pending);while (1){sigpending(&pending); //获取pendingprintPending(&pending); //打印pending位图(1表示未决)sleep(1);}return 0;
}
用户空间与内核空间
每个进程都能够看到操作系统,可以请求 OS去执行对应的系统调用。其实本质式只有OS的代码在运行,当有新进程进来时,OS统一管理,分配运行队列,同时该进程运行时间片到了,会执行进程的切换。
信号的捕捉 signal()和signaction()
进程收到信号之后,并不是立即处理信号,而是在从内核态切换回用户态的时候。此时还是内核态
如果待处理信号的处理动作是默认或者忽略:
如果待处理信号是自定义捕捉的:
7. linux下的线程
线程概念
进程并不是通过task_struct来衡量的,除了task_struct之外,一个进程还要有进程地址空间、文件、信号等等,合起来称之为一个进程。linux下的线程只是多创建了一个task_struct,而其它指向同一个file_struct结构和mm_struct结构,而信号位图也是相同的。只是在进程内部多了一个可以被cpu调度的执行流。
线程共享进程数据,因此所谓的代码段(Text Segment)、数据段(Data Segment)都是共享的:但也拥有自己的一部分数据:
- 线程ID。
- 一组寄存器。(存储每个线程的上下文信息)
- 栈。(每个线程都有临时的数据,需要压栈出栈)
- errno。(C语言提供的全局变量,每个线程都有自己的)
原生线程库pthread
在Linux中,站在内核角度没有真正意义上线程相关的接口,但是站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统为用户层提供了原生线程库pthread。
原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口,因此对于我们来讲,在Linux下学习线程实际上就是学习在用户层模拟实现的这一套接口,而并非操作系统的接口。
下面我们创建五个新线程后让这五个新线程将自己进行分离,那么此后主线程就不需要在对这五个新线程进行join等待了:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{pthread_detach(pthread_self());char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;}pthread_exit((void*)6666);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}while (1){printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());sleep(1);}return 0;
}
LWP和PID
当我们用ps axj
命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的。而使用ps -aL
命令,可以显示当前的轻量级进程。
LWP(Light Weight Process)就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们属于同一个进程。
注意: 在Linux中,应用层的线程与内核的LWP是一一对应的,实际上操作系统调度的时候采用的是LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。
线程ID及进程地址空间布局
线程的管理由线程库来实现,它需要有线程上下文,自己的线程栈以及线程局部存储。而线程ID本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程,也可以用它来访问到各个线程。
Linux线程互斥
pthread_mutex_t mutex;
pthread_mutex_init( ) ;
pthread_mutex_lock
例子:抢票程序中多个线程参与抢票,最后抢票数量为负数,因为--tickets包含三步操作,不是原子的。
要解决上述抢票系统的问题,需要做到三点:
- 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。
正确的抢票代码:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>int tickets = 1000;
pthread_mutex_t mutex;
void* TicketGrabbing(void* arg)
{const char* name = (char*)arg;while (1){pthread_mutex_lock(&mutex);if (tickets > 0){usleep(100);printf("[%s] get a ticket, left: %d\n", name, --tickets);pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}printf("%s quit!\n", name);pthread_exit((void*)0);
}
int main()
{pthread_mutex_init(&mutex, NULL);pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, TicketGrabbing, "thread 1");pthread_create(&t2, NULL, TicketGrabbing, "thread 2");pthread_create(&t3, NULL, TicketGrabbing, "thread 3");pthread_create(&t4, NULL, TicketGrabbing, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);return 0;
}
互斥锁的原理
OS提供了直接交换 寄存器和内存值 的一条汇编指令,mutex在内存中初始为1,第一个线程进来申请锁,先把寄存器的值置为0,然后把mutex与寄存器的内容交换,再判断寄存器的值是否是大于0。即使在这个过程中出现了线程切换也没有关系,寄存器的值还是会跟着线程上下文一起被带走。因此它可以保证申请锁是安全的。
什么叫做阻塞?
- 站在操作系统的角度,进程等待某种资源,就是将当前进程的task_struct放入对应的等待队列,这种情况可以称之为当前进程被挂起等待了。
- 站在用户角度,当进程等待某种资源时,用户看到的就是自己的进程卡住不动了,我们一般称之为应用阻塞了。
- 这里所说的资源可以是硬件资源也可以是软件资源,锁本质就是一种软件资源,当我们申请锁时,锁当前可能并没有就绪,可能正在被其他线程所占用,此时当其他线程再来申请锁时,就会被放到这个锁的资源等待队列当中。
线程同步
同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。
单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题。现在我们增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。
实现方法就是条件变量:
条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。
它会使得 线程 去某一个条件变量下进行等待,直到pthread_cond_signal,它被唤醒,再次查看条件是否满足,条件变量一定要与互斥量一起使用。
等待条件变量的代码
pthread_mutex_lock(&mutex);
while (条件为假)pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);
唤醒等待线程的代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
基于阻塞队列的生产消费模型
生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下:
- 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
- 两种角色: 生产者和消费者。(通常由进程或线程承担)
- 一个交易场所: 通常指的是内存中的一段缓冲区,可以是某个容器,vector或者queue。
我们用代码编写生产者消费者模型的时候,本质就是对这三个特点进行维护。
模拟实现基于阻塞队列的生产消费模型
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>#define NUM 5template<class T>
class BlockQueue
{
private:bool IsFull(){return _q.size() == _cap;}bool IsEmpty(){return _q.empty();}
public:BlockQueue(int cap = NUM): _cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_full, nullptr);pthread_cond_init(&_empty, nullptr);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_full);pthread_cond_destroy(&_empty);}//向阻塞队列插入数据(生产者调用)void Push(const T& data){pthread_mutex_lock(&_mutex);while (IsFull()){//不能进行生产,直到阻塞队列可以容纳新的数据pthread_cond_wait(&_full, &_mutex);}_q.push(data);pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_empty); //唤醒在empty条件变量下等待的消费者线程}//从阻塞队列获取数据(消费者调用)void Pop(T& data){pthread_mutex_lock(&_mutex);while (IsEmpty()){//不能进行消费,直到阻塞队列有新的数据pthread_cond_wait(&_empty, &_mutex);}data = _q.front();_q.pop();pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_full); //唤醒在full条件变量下等待的生产者线程}
private:std::queue<T> _q; //阻塞队列int _cap; //阻塞队列最大容器数据个数pthread_mutex_t _mutex;pthread_cond_t _full;pthread_cond_t _empty;
};
然后在主函数中创建生产者和消费者线程,并控制生产和消费的速度。这个模型中生产和消费不能同时进行,因为push和pop涉及对quque中size的修改,而它是非线程安全的。
信号量
我们可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,如果这些执行流访问的是临界资源的不同区域,那么我们可以让这些执行流同时访问临界资源的不同区域,此时不会出现数据不一致等问题。
int sem_init(sem_t *sem, int pshared, unsigned int value);
信号量的PV操作:
- P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。
- V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>class Sem{
public:Sem(int num){sem_init(&_sem, 0, num);}~Sem(){sem_destroy(&_sem);}void P(){sem_wait(&_sem); // sem_wait就是P操作}void V(){sem_post(&_sem);}
private:sem_t _sem;
};Sem sem(1); //二元信号量
int tickets = 2000;
void* TicketGrabbing(void* arg)
{std::string name = (char*)arg;while (true){sem.P();if (tickets > 0){usleep(1000);std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;sem.V();}else{sem.V();break;}}std::cout << name << " quit..." << std::endl;pthread_exit((void*)0);
}int main()
{pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}
基于环形队列的生产消费模型
blank_sem和data_sem的初始值设置
现在我们用信号量来描述环形队列当中的空间资源(blank_sem)和数据资源(data_sem),在我们初始信号量时给它们设置的初始值是不同的:
- blank_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间。
- data_sem的初始值我们应该设置为0,因为刚开始时环形队列当中没有数据
#pragma once#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>#define NUM 8template<class T>
class RingQueue
{
private://P操作void P(sem_t& s){sem_wait(&s);}//V操作void V(sem_t& s){sem_post(&s);}
public:RingQueue(int cap = NUM): _cap(cap), _p_pos(0), _c_pos(0){_q.resize(_cap);sem_init(&_blank_sem, 0, _cap); //blank_sem初始值设置为环形队列的容量sem_init(&_data_sem, 0, 0); //data_sem初始值设置为0}~RingQueue(){sem_destroy(&_blank_sem);sem_destroy(&_data_sem);}//向环形队列插入数据(生产者调用)void Push(const T& data){P(_blank_sem); //生产者关注空间资源_q[_p_pos] = data;V(_data_sem); //生产//更新下一次生产的位置_p_pos++;_p_pos %= _cap;}//从环形队列获取数据(消费者调用)void Pop(T& data){P(_data_sem); //消费者关注数据资源data = _q[_c_pos];V(_blank_sem);//更新下一次消费的位置_c_pos++;_c_pos %= _cap;}
private:std::vector<T> _q; //环形队列int _cap; //环形队列的容量上限int _p_pos; //生产位置int _c_pos; //消费位置sem_t _blank_sem; //描述空间资源sem_t _data_sem; //描述数据资源
};
因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:
- 环形队列为空时。
- 环形队列为满时。
但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:
- 当环形队列为空的时,消费者一定不能进行消费,因为此时数据资源为0。
- 当环形队列为满的时,生产者一定不能进行生产,因为此时空间资源为0。
也就是说,当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行。
Linux线程池
线程池是一种线程使用模式。
线程过多会带来调度开销,进而影响缓存局部和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。
线程池的优点
- 线程池避免了在处理短时间任务时创建与销毁线程的代价。
- 线程池不仅能够保证内核充分利用,还能防止过分调度。
线程池中的维护一个任务队列,初始化 启动num个线程,它不断检测任务队列是否为空,不为空则从任务队列取出任务来执行。而我们使用时则只需要往任务队列push任务T即可。
#pragma once#include <iostream>
#include <unistd.h>
#include <queue>
#include <pthread.h>#define NUM 5//线程池
template<class T>
class ThreadPool
{
private:bool IsEmpty(){return _task_queue.size() == 0;}void LockQueue(){pthread_mutex_lock(&_mutex);}void UnLockQueue(){pthread_mutex_unlock(&_mutex);}void Wait(){pthread_cond_wait(&_cond, &_mutex);}void WakeUp(){pthread_cond_signal(&_cond);}
public:ThreadPool(int num = NUM): _thread_num(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}//线程池中线程的执行例程static void* Routine(void* arg){pthread_detach(pthread_self());ThreadPool* self = (ThreadPool*)arg;//不断从任务队列获取任务进行处理while (true){self->LockQueue();while (self->IsEmpty()){self->Wait();}T task;self->Pop(task);self->UnLockQueue();task.Run(); //处理任务}}void ThreadPoolInit(){pthread_t tid;for (int i = 0; i < _thread_num; i++){pthread_create(&tid, nullptr, Routine, this); //注意参数传入this指针}}//往任务队列塞任务(主线程调用)void Push(const T& task){LockQueue();_task_queue.push(task);UnLockQueue();WakeUp();}//从任务队列获取任务(线程池中的线程调用)void Pop(T& task){task = _task_queue.front();_task_queue.pop();}
private:std::queue<T> _task_queue; //任务队列int _thread_num; //线程池中线程的数量pthread_mutex_t _mutex;pthread_cond_t _cond;
};
pthread_cond_broadcast函数的作用是唤醒条件变量下的所有线程,而外部可能只Push了一个任务,我们却把全部在等待的线程都唤醒了,此时这些线程就都会去任务队列获取任务,但最终只有一个线程能得到任务。一瞬间唤醒大量的线程可能会导致系统震荡,这叫做惊群效应。因此在唤醒线程时最好使用pthread_cond_signal函数唤醒正在等待的一个线程即可。
相关文章:
linux复习
1.关于进程 1.1 概念 用户角度:进程是程序的一次执行实例,也就是正在运行的程序 内核角度:操作系统分配内存和cpu资源的实体 操作系统使用内核数据结构 程序的代码及数据 描述进程,Linux中对应的内核数据结构就是task_struct…...
Post-Processing PropertySource instance详解 和 BeanFactoryPostProcessor详解
PropertySourcesBeanFactoryPostProcessor详解 1. 核心概念 BeanFactoryPostProcessor 是 Spring 框架中用于在 BeanFactory 初始化阶段 对 Environment 中的 PropertySource 进行后处理的接口。它允许开发者在 Bean 创建之前 对属性源进行动态修改,例如添加、删除…...
go 编译的 windows 进程(exe)以管理员权限启动(UAC)
引言 windows 系统,在打开某些 exe 的时候,会弹出“用户账户控制(UAC)”的弹窗 “你要允许来自xx发布者的此应用对你的设备进行更改吗?” UAC(User Account Control,用户账户控制)是 Windows 操作系统中的…...
Elasticsearch性能优化实践
一、背景与挑战 基金研报搜索场景中,我们面临以下核心挑战: 数据规模庞大:单索引超500GB原始数据,包含300万份PDF/Word研报文档查询性能瓶颈:复杂查询平均响应时间超过10秒,高峰期CPU负载达95%存储…...
【Web API系列】Web Shared Storage API 深度解析:WindowSharedStorage 接口实战指南
前言 在当今 Web 应用日益复杂的背景下,跨页面数据共享与隐私保护已成为现代浏览器技术演进的重要命题。传统 Web 存储方案(如 Cookies、LocalStorage)在应对多维度用户特征存储、跨上下文数据共享等场景时,逐渐暴露出技术瓶颈与…...
Eureka、LoadBalance和Nacos
Eureka、LoadBalance和Nacos 一.Eureka引入1.注册中心2.CAP理论3.常见的注册中心 二.Eureka介绍1.搭建Eureka Server 注册中心2.搭建服务注册3.服务发现 三.负载均衡LoadBalance1.问题引入2.服务端负载均衡3.客户端负载均衡4.Spring Cloud LoadBalancer1).快速上手2)负载均衡策…...
智能体MCP 实现数据可视化分析
参考: 在线体验 https://www.doubao.com/chat/ 下载安装离线体验 WPS软件上的表格分析 云上创建 阿里mcp:https://developer.aliyun.com/article/1661198 (搜索加可视化) 案例 用cline 或者cherry studio实现 mcp server:excel-mcp-server、quickchart-mcp-server...
3小时速通Python-Python学习总部署、总预览(一)
目录 Python的关键字有哪些: 编辑 代码:1-5: 代码:6-10: 代码:11-15: 代码:16-20: 代码:21-25: 代码:26-27: Pyt…...
机器学习基础 - 分类模型之决策树
决策树 文章目录 决策树简介决策树三要素1. 特征的选择1. ID32. C4.53. CART2. 剪枝处理0. 剪枝的作用1. 预剪枝2. 后剪枝QA1. ID3, C4.5, CART 这三种决策树的区别2. 树形结构为何不需要归一化?3. 分类决策树与回归决策树的区别4. 为何信息增益会偏向多取值特征?4. 为何信息…...
Java面向对象的三大特性
## 1. 封装(Encapsulation) 封装是将数据和操作数据的方法绑定在一起,对外部隐藏对象的具体实现细节。通过访问修饰符来实现封装。 示例代码: java public class Student { // 私有属性 private String name; private int age; …...
【Pandas】pandas DataFrame truediv
Pandas2.2 DataFrame Binary operator functions 方法描述DataFrame.add(other)用于执行 DataFrame 与另一个对象(如 DataFrame、Series 或标量)的逐元素加法操作DataFrame.add(other[, axis, level, fill_value])用于执行 DataFrame 与另一个对象&…...
GTS-400 系列运动控制器板(六)----修改编码器计数方向
运动控制器函数库的使用 运动控制器驱动程序、 dll 文件、例程、 Demo 等相关文件请通过固高科技官网下载,网 址为: www.googoltech.com.cn/pro_view-3.html 1 Windows 系统下动态链接库的使用 在 Windows 系统下使用运动控制器,首先要安装驱动程序。在安装前需要提…...
卷积神经网络迁移学习:原理与实践指南
引言 在深度学习领域,卷积神经网络(CNN)已经在计算机视觉任务中取得了巨大成功。然而,从头开始训练一个高性能的CNN模型需要大量标注数据和计算资源。迁移学习(Transfer Learning)技术为我们提供了一种高效解决方案,它能够将预训练模型的知识…...
Django 入门实战:从环境搭建到构建你的第一个 Web 应用
Django 入门实战:从环境搭建到构建你的第一个 Web 应用 恭喜你选择 Django 作为你学习 Python Web 开发的起点!Django 是一个强大、成熟且功能齐全的框架,非常适合构建中大型的 Web 应用程序。本篇将通过一个简单的例子,带你走完…...
【后端】构建简洁的音频转写系统:基于火山引擎ASR实现
在当今数字化时代,语音识别技术已经成为许多应用不可或缺的一部分。无论是会议记录、语音助手还是内容字幕,将语音转化为文本的能力对提升用户体验和工作效率至关重要。本文将介绍如何构建一个简洁的音频转写系统,专注于文件上传、云存储以及…...
http通信之axios vs fecth该如何选择?
在HTTP通信中,axios和fetch都是常用的库或原生API用于发起网络请求。两者各有特点,适用于不同的场景。下面详细介绍它们的差异和各自的优势: fetch 特点: 原生支持:fetch是现代浏览器内置的API,不需要额外…...
iostat指令介绍
文章目录 1. 功能介绍2. 语法介绍3. 应用场景4. 示例分析 1. 功能介绍 iostat (input/output statistics),是 Linux/Unix 系统中用于监控 CPU 使用率和 磁盘 I/O 性能的核心工具,可实时展示设备负载、吞吐量、队列状态等关键指标。 可以使用 man iostat查…...
NLP高频面试题(五十)——大模型(LLMs)分词(Tokenizer)详解
在自然语言处理(NLP)任务中,将文本转换为模型可处理的数字序列是必不可少的一步。这一步通常称为分词(tokenization),即把原始文本拆分成一个个词元(token)。对于**大型语言模型(LLM,Large Language Model,大型语言模型)**而言,选择合适的分词方案至关重要:分词的…...
桌面我的电脑图标不见了怎么恢复 恢复方法指南
在Windows操作系统中,“我的电脑”或在较新版本中称为“此电脑”的图标,是访问硬盘驱动器、外部存储设备和系统文件的重要入口。然而,有些用户可能会发现桌面上缺少了这个图标,这可能是由于误操作、系统设置更改或是不小心删除造成…...
【Qt】控件的理解 和 基础控件 QWidget 属性详解(通俗易懂+附源码+思维导图框架)
每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论: 通过上一章对信号槽的理解相信你对Qt的认识肯定有了很大的进步,下面将通过本篇文章带你深入的认识Widget控件(主窗口࿰…...
oracle将表字段逗号分隔的值进行拆分,并替换值
需求背景:需要源数据变动,需要对历史表已存的字段值根据源数据进行更新。如果是单字段存值,直接根据映射表关联修改即可。但字段里面若存的值是以逗号分割,比如旧值:‘old1,old2,old3’,要根据映射关系调整…...
用c语言实现——一个带头节点的链队列,支持用户输入交互界面、初始化、入队、出队、查找、判空判满、显示队列、遍历计算长度等功能
一、知识介绍 带头节点的链队列是一种基于链表实现的队列结构,它在链表的头部添加了一个特殊的节点,称为头节点。头节点不存储实际的数据元素,主要作用是作为链表的起点,简化队列的操作和边界条件处理。 1.节点结构 链队列的每…...
webpack基础使用了解(入口、出口、插件、加载器、优化、别名、打包模式、环境变量、代码分割等)
目录 1、webpack简介2、简单示例3、入口(entry)和输出(output)4、自动生成html文件5、打包css代码6、优化(单独提取css代码)7、优化(压缩过程)8、打包less代码9、打包图片10、搭建开发环境(webpack-dev-server…...
【项目】基于MCP+Tabelstore架构实现知识库答疑系统
基于MCPTabelstore架构实现知识库答疑系统 整体流程设计(一)Agent 架构(二)知识库存储(1)向量数据库Tablestore(2)MCP Server (三)知识库构建(1&a…...
C语言高频面试题——malloc 和 calloc区别
在 C 语言中,malloc 和 calloc 都是用于动态内存分配的函数,但它们在 内存初始化、参数形式 和 使用场景 上有显著区别。以下是详细的对比分析: 1. 函数原型 malloc void* malloc(size_t size);功能:分配 未初始化 的连续内存块…...
深入探讨JavaScript性能瓶颈与优化实战指南
JavaScript作为现代Web开发的核心语言,其性能直接影响用户体验与业务指标。随着2025年前端应用的复杂性持续增加,性能优化已成为开发者必须掌握的核心技能。本文将从性能瓶颈分析、优化策略、工具使用三个维度,结合实战案例,系统梳理JavaScript性能优化的关键路径。 一、Ja…...
[创业之路-376]:企业法务 - 创业,不同的企业形态,个人承担的风险、收益、税费、成本不同
在企业法务领域,创业时选择不同的企业形态,个人在风险承担、收益分配、税费负担及运营成本方面存在显著差异。以下从个人独资企业、合伙企业、有限责任公司、股份有限公司四种常见形态展开分析: 一、个人承担的风险 个人独资企业 风险类型&…...
【Lua】Lua 入门知识点总结
Lua 入门学习笔记 本教程旨在帮助有编程基础的学习者快速入门Lua编程语言。包括Lua中变量的声明与使用,包括全局变量和局部变量的区别,以及nil类型的概念、数值型、字符串和函数的基本操作,包括16进制表示、科学计数法、字符串连接、函数声明…...
低空经济 WebGIS 无人机配送 | 图扑数字孪生
2024 年,”低空经济” 首次写入政府工作报告,在政策驱动下各地纷纷把握政策机遇,从基建网络、场景创新、产业生态、政策激励等方面,构建 “规划-建设-应用-赋能” 的系统性布局,作为新质生产力的重要体现,推…...
【程序员 NLP 入门】词嵌入 - 如何基于计数的方法表示文本? (★小白必会版★)
🌟 嗨,你好,我是 青松 ! 🌈 希望用我的经验,让“程序猿”的AI学习之路走的更容易些,若我的经验能为你前行的道路增添一丝轻松,我将倍感荣幸!共勉~ 【程序员 NLP 入门】词…...
基于机器学习的多光谱遥感图像分类方法研究与定量评估
多光谱遥感技术通过获取可见光至红外波段的光谱信息,为地质勘探、农业监测、环境调查等领域提供了重要支持。与普通数码相机相比,多光谱成像能记录更丰富的波段数据(如近红外、短波红外等),从而更精准地识别地物特征。…...
BEVDepth: Acquisition of Reliable Depth for Multi-View 3D Object Detection
背景 基于多视角图片的3D感知被LSS证明是可行的,它使用估计的深度将图像特征转化为3D视椎,再将其压缩到BEV平面上。对于这个得到的BEV特征图,它支持端到端训练以及各种下游任务。但是对于深度估计这一块学习的深度质量如何,到目前为止没有相关工作研究。 贡献 本文的贡献…...
【Linux】静态库 动态库
🌻个人主页:路飞雪吖~ 🌠专栏:Linux 目录 一、👑静态库和动态库 静态库: 动态库: 🌠手动制作静态库 && 手动调用一下我们自己写的静态库 1> 安装到系统里面 ✨生成静…...
Java转Go日记(六):TCP黏包
服务端代码如下: // socket_stick/server/main.gofunc process(conn net.Conn) {defer conn.Close()reader : bufio.NewReader(conn)var buf [1024]bytefor {n, err : reader.Read(buf[:])if err io.EOF {break}if err ! nil {fmt.Println("read from client…...
(51单片机)LCD显示温度(DS18B20教程)(LCD1602教程)(延时函数教程)(单总线教程)
演示视频: LCD显示温度 源代码 如上图将9个文放在Keli5 中即可,然后烧录在单片机中就行了 烧录软件用的是STC-ISP,不知道怎么安装的可以去看江科大的视频: 【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.…...
【通过Docker快速部署Tomcat9.0】
文章目录 前言一、部署docker二、部署Tomcat2.1 创建存储卷2.2 运行tomcat容器2.3 查看tomcat容器2.4 查看端口是否监听2.5 防火墙开放端口 三、访问Tomcat 前言 Tomcat介绍 Tomcat 是由 Apache 软件基金会(Apache Software Foundation)开发的一个开源 …...
云原生--基础篇-3--云原生概述(云、原生、云计算、核心组成、核心特点)
1、什么是云和原生 (1)、什么是云? “云”指的是云计算环境,代表应用运行的基础设施和资源。依赖并充分利用云计算的弹性、分布式和资源池化能力。 核心含义: 1、云计算基础设施 云原生应用的设计和运行完全基于云…...
Spark-Streaming
Spark-Streaming概述 DStream实操 案例一:WordCount案例 需求:使用 netcat 工具向 9999 端口不断的发送数据,通过 SparkStreaming 读取端口数据并统计不同单词出现的次数 实验步骤: 添加依赖 <dependency> <gro…...
乐视系列玩机------乐视2 x620红灯 黑砖刷写教程以及新版刷写工具的详细释义
乐视x620在上期解析了普通黑砖情况下的救砖刷机过程。但在一些例外的情况下。使用上面的步骤会一直刷写报错 。此种情况就需要另外一种强制刷写方法来救砖 通过博文了解💝💝💝 1💝💝💝-----详细解析乐视2 x620系列 红灯 黑砖线刷救砖的步骤 2💝💝💝----图…...
若依SpringCloud项目-定制微服务模块
若依SpringCloud项目-定制微服务模块 关于微服务先不过多介绍,刚开始熟悉并不能讲的很彻底,成熟的微服务项目-若依SpringCloud就是一个典型的微服务架构工程(网上有很多教程了,不明白的可以学习一下)。 我正在看的视…...
【扫描件批量改名】批量识别扫描件PDF指定区域内容,用识别的内容修改PDF文件名,基于C++和腾讯OCR的实现方案,超详细
批量识别扫描件PDF指定区域内容并重命名文件方案 应用场景 本方案适用于以下场景: 企业档案数字化管理:批量处理扫描的合同、发票等文件,按内容自动分类命名财务票据处理:自动识别票据上的关键信息(如发票号码、日期)用于归档医疗记录管理:从扫描的检查报告中提取患者I…...
学习Docker遇到的问题
目录 1、拉取hello-world镜像报错 1. 检查网络连接 排查: 2. 配置 Docker 镜像加速器(推荐) 具体解决步骤: 1.在服务器上创建并修改配置文件,添加Docker镜像加速器地址: 2. 重启Docker 3. 拉取hello-world镜像 2、删除镜像出现异常 3、 容器内部不能运行ping命令 …...
Docker 数据卷
目录 一、数据卷(Data Volume) 二、使用 1、单独建立数据卷 2、挂载主机数据卷 3、数据卷容器挂载 基本语法: 工作原理: 主要用途: 使用事例: 一、数据卷(Data Volume) 数据卷的使用,类似于 Linux 下对目录或文件进行 mount 数据卷(Data Volume)是一个可供一个或多…...
【数据结构】励志大厂版·初级(二刷复习)双链表
前引:今天学习的双链表属于链表结构中最复杂的一种(带头双向循环链表),按照安排,我们会先进行复习,如何实现双链表,如基本的头插、头删、尾删、尾插,掌握每个细节,随后进…...
通过dogssl申请ssl免费证书
SSL证书作为实现HTTPS加密的核心工具,能够确保用户与网站之间的数据传输安全。尤其是在小程序之类的开发时,要求必须通过https发起请求的情况下。学会如何免费申请一个ssl证书就很有必要了。这里我分享一下,我通过dogssl如何申请ssl的。 一&…...
路由与路由器
路由的概念 路由是指在网络通讯中,从源设备到目标设备路径的选择过程。路由器是实现这一过程的关键设备,它通过转发数据包来实现网络的互联。路由工作在OSI参考模型的第三层,‘网络层’。 路由器的基本原理 路由器通过维护一张路由表来决定…...
Docker底层原理浅析 | namespace+cgroups+文件系统
本文目录 1. Linux NamespaceLinux系统里是否只能有一个pid为1的进程?namespace机制查看namespacenamespace机制测试使用Docker验证namespace机制 2. Dcoerk网络模式3.Control groups4.文件系统(联合文件系统)5. 容器格式 1. Linux Namespace…...
【无人机】使用扩展卡尔曼滤波 (EKF) 算法来处理传感器测量,各传感器的参数设置,高度数据融合、不同传感器融合模式
目录 #1、IMU #2、磁力计 #3、高度 #典型配置 #4、气压计 #静压位置误差修正 #气压计偏置补偿 #5、全球导航系统/全球定位系统--GNSS/GPS #位置和速度测量 #偏航测量 #GPS 速度的偏航 #双接收器 #GNSS 性能要求 #6、测距 #条件范围辅助-Conditional range aidin…...
常见的raid有哪些,使用场景是什么?
RAID(Redundant Array of Independent Disks,独立磁盘冗余阵列)是一种将多个物理硬盘组合成一个逻辑硬盘的技术,目的是通过数据冗余和/或并行访问提高性能、容错能力和存储容量。不同的 RAID 级别有不同的实现方式和应用场景。以下…...
《 C++ 点滴漫谈: 三十四 》从重复到泛型,C++ 函数模板的诞生之路
一、引言 在 C 编程的世界里,类型是一切的基础。我们为 int 写一个求最大值的函数,为 double 写一个相似的函数,为 std::string 又写一个……看似合理的行为,逐渐堆积成了难以维护的 “函数墙”。这些函数逻辑几乎一致࿰…...