进程第1篇--进程基本知识
一、进程和程序的概念区别
程序是不占内存的,进程才占
二、并发
得益于CPU强、操作系统时间片轮转机制,可以让多个进程同时处于运行状态。
注意:
单核cpu,同一时刻,只有一个进程在运行;
多核cpu之间,是真正的并发。
操作系统的中断处理是并发的基础,强制让进程暂时放弃当前系统资源。
三、系统资源基础
程序从硬盘拷贝到内存,进程启。
四、内存管理单元MMU / 虚拟内存和物理内存的映射关系
一个进程运行时都会得到4G的虚拟内存。这个虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。
每个人都是亿万富翁,实际上一共也就只有一亿。
04进程学习之详解虚拟内存通过MMU映射到物理内存和MMU对物理内存分级(或者叫虚拟内存和物理内存的映射关系)_Mango酱的博客-CSDN博客04进程学习之详解虚拟内存通过MMU映射到物理内存和MMU对物理内存分级(或者叫虚拟内存和物理内存的映射关系)首先,我们先了解什么是虚拟内存和物理内存。1 虚拟内存和物理内存1)虚拟内存:虚构出来的,不是真实的内存,大小为32位0-4G。2)物理内存:真正的内存。一般大小远远小于4G,一般有512M,1024M等。注:(接下来的物理内存皆以512为例)2 两者的关系都是内存,虚拟内存只是虚构的不存在的,它之所以能存放这么大的东西(实际不存放,或者称之为记录)是通过MMU映射的结果。而物理内存才https://blog.csdn.net/weixin_44517656/article/details/109374526?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169286275416800182758086%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169286275416800182758086&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-4-109374526-null-null.142%5Ev93%5EchatgptT3_2&utm_term=%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E5%92%8C%E7%89%A9%E7%90%86%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84%E5%85%B3%E7%B3%BB&spm=1018.2226.3001.4187>>>MMU的内存分级对进程内部数据读取效率的影响
我们知道用户部分的内存访问内核的内存即系统调用时比较慢,而内核访问用户区内存是非常快的,那么为什么呢?
这就是我们的MMU除了映射虚拟内存,还有另一个作用就是对内存分级。Windows下MMU对物理内存分3级,分别为0,1,2,越低级别越高;Linux下分0,1,同样也是越低越高。低级别的访问高级别的内存需要等待相应才能访问,而高级别访问低级别可以直接访问。
所以当用户访问内核的虚拟内存时,需要等待内核的响应才能访问被MMU映射的物理内存;而当内核访问用户区时,不需要等待即可立即访问其物理内存。
这就是MMU的另一个重要作用,防止用户频繁访问内核。
————————————————
版权声明:本文为CSDN博主「Mango酱」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44517656/article/details/109374526
虚拟内存的最大上限
一个计算机系统的虚拟存储器,其:最大容量由CPU的地址长度决定;实际容量为内存和外存容量之和。
堆的最大可分配大小上限是由虚拟内存的最大值决定的。虚拟内存的最大值由Min(内存容量和外存容量之和,计算机的地址位数能容纳的最大容量) 决定。
五、进程间为什么不能通信
所有进程看到的这个地址都是一样的,里面的内存都是从 0 开始编号。(进程不直接操作物理地址,操作系统给进程分配虚拟地址。同时操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。)
操作系统的内存管理,主要分为三个方面。
第一,虚拟内存空间的管理,每个进程看到的是独立的、互不干扰的虚拟地址空间;
第二,物理内存的管理,物理内存地址只有内存管理模块能够使用;
第三,内存映射,需要将虚拟内存和物理内存映射、关联起来。
对于内存的访问,用户态的进程使用虚拟地址;内核态的也基本都是使用虚拟地址;
虚拟地址到物理地址的映射表,这个是内存管理模块的一部分。
虚拟内存的分配:
一部分用来放内核的东西,称为内核空间,一部分用来放进程的东西,称为用户空间。且进程无法直接访问内核空间。
用户空间在下,在低地址;内核空间在上,在高地址。
用户态和内核态的划分:
整个虚拟内存空间要一分为二,一部分是用户态地址空间,一部分是内核态地址空间。32 位系统,最大能够寻址 2^32=4G,其中用户态虚拟地址空间是 3G,内核态是 1G。
进程的用户空间内存布局:
进程的用户空间内存布局通常可以分为以下几个部分:
-
代码段(Text Segment):也称为可执行代码区,存放着程序的机器指令。这部分内存通常是只读的,因为程序在运行时不应该被修改。
-
数据段(Data Segment):存放着已经初始化的全局变量和静态变量。这部分内存通常是可读写的。
-
BSS段(Block Started by Symbol):存放着未初始化的全局变量和静态变量。这些变量会在程序启动时被自动初始化为0或空值。
-
堆(Heap):用于动态分配内存。在堆中,程序可以进行手动的内存分配和释放,通过函数如malloc()和free()来实现。
-
栈(Stack):用于存储函数调用的局部变量、临时变量以及函数调用的上下文信息。栈是一种自动管理内存的机制,当一个函数被调用时,相关的局部变量会被分配到栈上;而当函数返回时,这些变量会自动被释放。
-
环境变量区域(Environment Variables):存放着程序运行时所需的环境变量信息。
需要注意的是,不同操作系统和编程语言可能会有一些细微的差异,但通常以上这些部分是一个典型的用户空间内存布局。
内核空间:
内核里面,无论是从哪个进程进来的,看到的都是同一个内核空间,看到的都是同一个进程列表,但内核栈是各用各的。
————————————————
版权声明:本文为CSDN博主「深山猿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/h2604396739/article/details/120032903
六、Linux系统常见的进程有哪些
1、linux系统进程类型有 :交互进程 ;批处理进程 ;监控进程(守护进程);
交互进程:由一个shell启动的进程。交互进程既可以在前台运行,也可以在后台运行。
批处理进程:这种进程和终端没有联系,是一个进程序列。
监控进程(也称守护进程):Linux系统启动时启动的进程,并在后台运行。
2、常见的进程
1./usr/sbin/inetd 守护进程为网络提供 Internet 服务管理。
2.Linux下有3个特殊的进程,idle进程(PID=0), init进程(PID=1)和kthreadd(PID=2)
(1)idle进程由系统自动创建,运行在内核态.idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换.
(2)init进程由idle通过kernel_thread创建,在内核空间完成初始化后,加载init程序,并最终用户空间创建 .init 进程 (pid = 1, ppid = 0),init进程由0进程创建,完成系统的初始化.是系统中所有其它用户进程的祖先进程.
(3) kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间,负责所有内核线程的调度和管理 .kthreadd (pid = 2, ppid = 0)它的任务就是管理和调度其他内核线程kernel_thread,会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread,当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程.
————————————————
版权声明:本文为CSDN博主「魏波.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weibo1230123/article/details/82187572
七、PCB进程控制块
每一个进程都有一个PCB存在OS内核区,维护进程信息。
进程控制块是存放进程的管理和控制信息的一个三百多行的 结构体(基本描述了进程的一切)。
在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤销而撤销。
PCB主要包括四个方面的信息:
1、进程描述信息(PID,UID)
2、进程控制信息(priority)
3、所拥有的资源和使用情况
4、Cpu现场信息
展开来讲:
1、进程描述信息:
进程标识符(Process ID),唯一通常为整数
进程名,通常基于可执行文件名,不唯一
用户标识符(User ID),记录创建进程的用户
进程组关系,记录该进程的父进程、子进程等关系
2、进程控制信息:
当前状态
优先级(priority)
代码执行入口地址
程序的磁盘地址
运行统计信息(执行时间、页面调度)
进程间同步和通信
进程的队列指针
进程的消息队列指针
3、所拥有的资源和使用情况:
虚拟地址空间状况
打开文件列表
4、CPU现场信息:
(进程运行的时候,操作系统需要保存的硬件执行状态信息)
寄存器值(通用寄存器、程序计数器PC、程序状态字PSW、栈指针)
指向该进程页表的指针
————————————————
版权声明:本文为CSDN博主「一只特立独行的老猫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_42285271/article/details/88018026
重点分解以下几个进程控制块中的变量概念:
(一)进程运行状态
导致转换的事件:
空->新建:创建执行一个程序的新进程,可能的事件有:新的批处理作业、交互登录(终端用户登录到系统)、操作系统因为提供一项服务而创建、由现有的进程派生等。
新建->就绪:操作系统准备好再接纳一个进程时,把一个进程从新建态转换为就绪态。
就绪->运行:需要选择一个新进程运行时,操作系统的调度器或分配器根据某种调度算法选择一个处于就绪态的进程。
运行->退出:导致进程终止的原因有:正常完成、超过时限、系统无法满足进程需要的内存空间、进程试图访问不允许访问的内存单元(越界)、算术错误(如除以0或存储大于硬件可以接纳的数字)、父进程终止(操作系统可能会自动终止该进程所有的后代进程)、父进程请求终止后代进程等。
运行->就绪:最常见的原因是,正在运行的进程到达了“允许不中断执行”的最大时间段,该把处理器的资源释放给其他在就绪态的进程使用了;还有一中原因可能是由于具有更改优先级的就绪态进程抢占了该进程的资源,使其被中断转换到就绪态。
运行->阻塞:如果进程请求它必须等待的某些事件,例如一个无法立即得到的资源(如I/O操作),只有在获得等待的资源后才能继续进程的执行,则进入等待态(阻塞态)。
阻塞->就绪:当等待的事件发生时,处于阻塞态的进程转换到就绪态。
就绪->退出:在上图中没有标出这种转换,在某些进程中,父进程可以在任何时刻终止一个子进程,如果一个父进程终止,所有相关的子进程都被终止。
阻塞->退出:跟上一项原因类似。
(二)进程的标识符(Identifiers)
每个进程有进程标识符(PID)、用户标识符、组标识符。
PID是一个正整数,取值范围从2到32768
可以通过:cat /proc/sys/kernel/pid_max 查看系统支持多少进程
当一个进程被启动时,OS会顺序挑选下一个未使用的编号数字做为该进程的PID。进程号可以重复使用;即一个进程结束后,该进程的进程号会被OS回收并再发放给另一个要运行的进程。
(三)进程的文件描述符表
- 每个进程都有一个文件描述符表。OS内核中,每个PCB中都有一个指针成员,这个指向一个表,叫做这个进程的 文件描述符表。
- 这个表是一个键值数组。键是进程打开的文件的 int型数据,文件描述符fd;值是被打开的文件结构体指针。
- 进程打开一个文件后,获得一个fd(文件描述符最大1023,说明一个进程最多能打开1024个文件。新打开的得到的键即fd的大小,一定是所有文件描述符表中可用的, 最小的那个文件描述符)。
- 关闭一个文件,它占用的fd就被释放了,以后可以被重新占用。
-
每一次新建一个进程,OS会为这个进程自动打开下面3个文件,占用0 1 2 三个描述符;因此新打开的第一个其他文件的fd一定是3,然后往后排。
0:标准输入
1:标准输出
2:标准错误
- 进程想操作这个文件,只拿着一个fd就行了。fd和的对应关系 进程不感知,只有OS知道(应该是避免用户直接修改这个结构体的成员值)。
- 文件是所有进程共享的,两个进程可以打开同一个文件。
(四) 新建文件的权限掩码:umask
用处:系统IO时,open新建一个文件时,设置文件权限时会用到这个变量
八、进程控制(fork & wait())
(一)fork函数原理
重点关注:
(1)如果fork成功后,父子进程两块相同代码的fork行,都会返回一个值。父进程返回子进程的pid。子进程返回0,继续往下执行。失败返回-1并设置errno。
(2)创建进程有两种方式,一是由操作系统创建(只有0号进程是系统构造的);二是由父进程创建(其他全是fork出来的)。
整个Linux系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的0号进程,他是所有进程的祖先。
fork原理:
执行fork之后,子进程被拷贝出来;
现在就是两个进程,都从fork往下执行;
关于fork的返回,父进程的fork本身返回子进程的pid,子进程的fork返回给父进程0。
验证试验:
注意:使用if else 的pid的来区分父子进程,很好用!
结果实验:
结果分析:
1、前四个printf打印是父进程的;后四个打印两个是父进程,两个是子进程的
2、后面四个,一三是父进程的;二四是子进程的。验证了两个pid,以及两个进程都会往下执行。
(二)获取进程pid的两个函数 getpid() getppid()
getpid():当前进程
getppid():当前进程的父进程
注意:bash上直接执行的进程,其父进程是bash
(三)如何利用fork函数,通过循环,创建5个子进程【要求:手写实操】
注意:被创建的子进程本身,也会迭代递归再创建,这不是我们想要的
验证代码:
实验结果:
结果分析:
1、子进程打印顺序不是按顺序?
通过for循环创建子进程之后,可以认为是同一时间。后面想屏幕输出打印时候,在争夺cpu
2、2 子进程打印在下一个命令终端提示符号之后?
父进程执行结束,父进程的父进程bash比2子进程抢先抢到了cpu,向屏幕输出
3、有没有办法控制 子进程被创建后按照先后输出?
使用sleep控制父子进程时序
(四)子进程在被fork出来之后拷贝了父进程哪些数据?
原则:“读时共享,写时复制”
以全局变量为例,也就是说:
1、如果代码中父子进程都不重写全局变量,那两个进程就用的一个全局变量
2、如果父进程改了全局变量,那么父进程就会新建一个全局变量在其用户空间,子进程还是还是读原来的
3、如果父子进程都新写,那就有三个全局变量。
(五)多进程编程调试-Gdb
使用gdb调试多进程程序的时候, gdb只能跟踪一个进程, 可以在fork函数调用之前通过指令设置gdb跟踪父进程还是子进程:
set follow-fork-mode child
set follow-fork-mode parent
(六)子进程换剧本:exec函数族
换核不换壳:pid之类的不变,换剧本了
也就是说,从子进程开始执行exec函数开始,子进程用户空间的代码和数据就被换掉了
第一个最被频繁使用:
函数用法:
//p表示程序加载需要借助path环境变量, 所以该函数通常用来调用系统程序。比如ls、data、cat
int execlp(const char* file, const char* arg, ... /* (char *) NULL */);//...表示变参
//直接指定可执行文件路径
int execl(const char *path, const char *arg, .../* (char *) NULL */);
//v是vector的意思,就是将execlp中的参数组织成字符串数组传入(或许你也可以传入从main函数中传来的参数)
int execvp(const char *file, char *const argv[]);
//注意结尾加上NULL指定变参结束, printf函数也是变参, 结尾也要加上NULL作为哨兵
/*execlp("ls","ls","-l","-R","-h",NULL)的等效形式*/
char* argv[]={"ls","-l","-R","-h",NULL};//传参:文件名、若干个参数...、NULL结尾
execvp("ls",argv);
实验代码:
exec
函数族的一般规律:
思考:我们知道我们自己写的程序要执行起来,bash是父进程。那么是怎么实现的呢?
bash先fork,然后再exec
小练习:将ps aux
的输出打印到文件当中:(应用了 open exec dup2)
//execlp-ps.c
int main(int argc, char* argv[]) {
int fd = open("ps.log", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perr_exit("open error");
}
int ret = dup2(fd, STDOUT_FILENO);
if (ret < 0) {
perr_exit("dup2 error");
}
execlp("ps", "ps", "-aux", NULL);
perr_exit("execlp error");
return 0;
}
九、孤儿进程和僵尸进程(对立概念)
(零)进程常用命令 ---ps和kill
ps aux|grep
ps ajx
: 查看进程ID和父进程ID
kill -9 pid
通常情况下,我们使用的-l(信号)的时候比较多,如我们前文提到的kill -9中的9就是信号。
HUP 1 终端断线
INT 2 中断(同 Ctrl + C)
QUIT 3 退出(同 Ctrl + \)
TERM 15 终止
KILL 9 强制终止
CONT 18 继续(与STOP相反, fg/bg命令)
STOP 19 暂停(同 Ctrl + Z)
(一)孤儿进程:
父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程(进程孤儿院)领养。
孤儿进程demo:
运行结果:父进程死了之后,子进程的PPID改变了,是init的
孤儿进程有什么问题:
孤儿进程并不会有什么危害,因为它们会被 init 进程收养并由 init 进程对它们完成状态收集工作。但是,如果孤儿进程数量过多,会占用系统资源,导致系统运行缓慢。此外,如果孤儿进程一直没有结束,可能会导致系统中出现僵尸进程。
(二)僵尸进程:
子进程运行完了,父进程一直在运行(子进程就无法进孤儿院),没来得及对子进程进行回收期间,子进程为“僵尸进程”。
回收僵尸进程,直接kill无效。得 kill 它的父进程,让init进程去回收它。
这里要注意,每个进程结束后都必然会经历僵尸态,时间长短的差别而已。子进程终止运行时,其 PCB 还存放于内核中,(PCB 记录了进程结束原因)P进程回收就是回收 PCB。
(三)父进程回收子进程的方式--wait() (阻塞等待)函数
在每个进程退出(注意:进程退出和回收不同)的时候,内核释放该进程的几乎所有的资源,用户区的所有资源。包括打开的文件、占用的内存等,但仍然为其保留一定的信息,这些信息主要指进程控制块PCB的信息(进程号、进程退出状态)。如果子进程不是孤儿,父进程有义务回收其子进程。
父进程通过调用wait()或者waitpid() 函数 得到它的退出状态,同时彻底回收掉这个进程。
wait()函数:
注意:status 用来把子进程的退出状态传出来,然后用以下宏函数进一步解析出来。
父进程调用
wait
函数可以回收子进程终止信息, 该函数有三个功能:
- 阻塞等待(wait)子进程退出(如果父进程调用wait的时候,子进程还没死,父进程就等着,轮询查看子进程死没死)
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)(执行完后,子进程的所有痕迹就彻底消失。)
Demo:
//wait-demo.c
int main(int argc, char* argv[]) {
pid_t p1 = fork();
if (p1 == 0) {
printf("I'm child, I'm going to sleep 20s\n");
sleep(20);
printf("I'm child, I'm going to die\n");
return 111;//子进程如果正常退出,则status值为111;没有return,则为0
} else if (p1 > 0) {
int status;
printf("I'm parent\n");
pid_t p2 = wait(&status);
if (p2 < 0) {
perr_exit("wait error");
}
if (WIFEXITED(status)) { //获取子进程退出状态
printf("my child exited with %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("my child was killed by %d\n", WTERMSIG(status));
}
printf("I'm parent, wait %d == %d finished\n", p1, p2);
} else {
perr_exit("fork error");
}
return 0;
}
当子进程被kill -9 信号杀死,输出结果:
$ ./a.out
I'm parent
I'm child, I'm going to sleep 10s
my child was killed by 9
I'm parent, wait 23663 == 23663 finished
进程的所有异常终止都是因为信号
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
waitpid函数
waitpid功能更为强大,
可以指定某一个子进程进行回收,可以设置非阻塞回收。- 一次
wait
或waitpid
函数调用, 只能回收一个子进程。即如果循环创建了多个子进程, 也只能通过循环回收所有子进程。pid_t waitpid(pid_t pid, int* wstatus, int options); //返回值 >0:被回收的子进程pid ==0:函数调用时,参三指定了 WNOHANG,并且没有子进程结束。即waitpid啥也没回收到 -1:回收失败。设置errno //传入参数 参1传要回收的pid, 传-1表示回收任意子进程, 传0表示回收同一组的所有子进程; 参2传进程结束状态, 如果不关心直接传NULL(传出参数); 参3传回收方式:WNOHANG(非阻塞);
Demo:
//waitpid-demo.c
//指定干掉第3个子进程
int main(int argc, char* argv[]) {
int i = 0;
pid_t p2;
for (; i < 5; ++i) {
pid_t p = fork();
if (p < 0) { //错误
perr_exit("fork error");
} else if (p > 0) { //父进程中
if (i == 2) {
p2 = p;
}
} else { //子进程中
break;
}
}
if (i == 5) {
// sleep(2);
pid_t wpid = waitpid(p2, NULL, 0);
if (wpid < 0) {
perr_exit("waitpid error");
} else {
printf("waitpid a child %d\n", wpid);
}
} else {
sleep(1);
printf("I'm %d child, mypid = %d\n", i, getpid());
}
return 0;
}
waitpid回收多个子进程: 用while
循环
//waitpid-while.c
int main(int argc, char* argv[]) {
int i = 0;
for (i = 0; i < 5; ++i) {
pid_t pid = fork();
if (pid < 0) {
perr_exit("fork error");
} else if (pid == 0) {
break;
}
}
pid_t wpid;
if (i == 5) { //每次无差别回收一个,直到回收失败
while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) { //传参-1
if (wpid == 0) {
sleep(1);
continue;
} else if (wpid > 0) {
printf("catch child %d\n", wpid);
}
}
} else {
sleep(1);
printf("I'm %d child, mypid = %d\n", i, getpid());
}
return 0;
}
进程第2篇--进程间通信(IPC)
根据第1篇 第四章,进程地址空间独立,互不可见。
进程之间的数据交换,只能通过内核。----进程间通信基本原理
常用 进程间通信方式 优缺点比较
进程间通信1 --管道
*重点* 匿名管道pipe(4K)
管道的概念:
管道是一种最基本的IPC机制, 作用于有血缘关系(父子、兄弟、叔侄进程)的进程之间。调用pipe系统函数即可创建一个管道, 有如下特性:
- 其本质是一个伪文件(实际为内核使用环形队列机制, 借助内核缓冲区(4k)实现。向管道的读写数据其实是向内核缓冲区读写)
- 有两个文件描述符引用, 一个表示读端, 一个表示写端
- 规定数据从管道的写端流入管道, 从读端流出, 只能单向流动
管道的局限性:
- 管道中的数据不可反复读取, 一旦读走, 管道中不再存在。(队列特征)
- 采用半双工通信方式, 数据只能在单方向上流动数据;一端不能同时是读写端。
- 只能在有公共祖先的进程之间使用管道(因为读写fd不共享)
创建管道:
int pipe(int pipefd[2]); 入参:pipefd[0]-读端;pipefd[1]-写端 // 函数调用成功返回“ ‘读写’两个文件描述符在 fd[2]”,分别给后面两个进程打开关闭和读写函数使用。 返回值:成功返回0, 失败返回-1并设置errno
函数调用成功后,进程就创建了一个管道,管道两头,一读一写端,均打开。fork完子进程后,(与父进程共享文件描述符数组)就是如下状态:
两头需要手动关闭自己不需要用的另一端,然后就可以正常读写了。
最后完成后,记得把管道完全关闭。
代码实现管道 Demo -父进程写,子进程读,读完显示在标准输出:
管道的读写行为逻辑:
读管道:
1.管道中有数据, read返回实际读到的字节数
2.管道中无数据:
若管道写端被全部关闭, 则read返回0
若写端没有被全部关闭, 则read挂起让出CPU,等待数据来
写管道:
1.管道读端全部被关闭, 进程异常终止(也可以捕捉SIGPIPE信号, 使进程异常终止)
2.管道读端没有全部关闭:
若管道已满, 则write阻塞
若管道未满, 则write将数据写入, 并返回实际写入的字节数
————————————————
原文链接:https://blog.csdn.net/DanielSYC/article/details/118458055
*重点demo* 父子进程借助管道实现 ls|wc -l
统计行数的功能:
先熟悉 wc -l命令,统计标准输出中内容的行数。
再熟悉下组合命令:
个人思路:写一个程序,ls 标准输出内容重定向到管道输入端。 程序创建一个子进程,这个进程exec wc -l,文件内容来自管道,把命令输出显示到屏幕上。
int main(int argc, char* argv[]) {
int pfd[2];
int ret = pipe(pfd);
if (ret < 0) {
perr_exit("pipe error");
}
pid_t pid = fork();
if (pid < 0) {
perr_exit("fork error");
} else if (pid > 0) { //父进程实现ls内容写到管道
close(pfd[0]);
dup2(pfd[1], STDOUT_FILENO);//父进程先将标准输出重定向到管道写端
execlp("ls", "ls", NULL);//然后执行ls
perr_exit("execlp error");
} else { // 子进程实现将标准输入重定向到
close(pfd[1]);
dup2(pfd[0], STDIN_FILENO);//子进程将标准输入重定向到管道读端
execlp("wc", "wc", "-l", NULL);//执行wc -l命令;读标准输入就相当于读管道
perr_exit("execlp error");
}
return 0;
}
*重点demo* 使用兄弟进程实现上面demo:
int main(int argc, char* argv[]) {
int pfd[2];
int ret = pipe(pfd);
if (ret < 0) {
perr_exit("pipe error");
}
int i = 0;
for (; i < 2; ++i) { //循环创建n个子进程
pid_t pid = fork();
if (pid < 0) {
perr_exit("fork error");
} else if (pid == 0) {
break;
}
}
//下面这个if分支是三个进程各认领执行各自的
if (i == 0) {
close(pfd[0]);
dup2(pfd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
perr_exit("execlp error");
} else if (i == 1) {
close(pfd[1]);
dup2(pfd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
perr_exit("execlp error");
} else { // parent //管道是在父进程中创建,随后又fork两个子进程。
close(pfd[0]), close(pfd[1]);//没关之前,有三个进程连两头;因此把父进程的两头干掉
wait(NULL), wait(NULL);
}
return 0;
}
注意:保证数据干脆的单向准确流动,把父进程的两头断开。
实际上允许多端输入,一端读取。只是时序不好把握
命名管道fifo(内核缓冲区、消息队列、一次读取)
匿名管道pipe的优缺点:
fifo是在pipe的基础上,给fifo在文件系统中创建一个inode(它会在文件系统中有一个文件名---这应该是命名管道名字的由来)。FIFO是Linux基础文件类型中的一种, 但是FIFO文件在磁盘上依然没有数据块, 仅仅用来标识内核中的一条通道,文件的内容还是在内核中。(FIFO的使用与文件极为相似,一旦创建了FIFO,各进程可以打开open这个“文件”进行read/write), 实际上是在读写内核通道, 这样就实现了进程间通信。
(因为fifo有实际文件名作为通信管道,为区分pipe, 将FIFO称为命名管道)。FIFO可以用于无血缘的进程间交换数据。
FIFO的创建:
函数方式:
int mkfifo(const char* pathname, mode_t mode); //入参:新建的FIFO路径文件名,文件的读写权限(umask) //成功返回0, 失败返回-1并设置errno;
命令方式:
mkfifo 管道名
FIFO使用的Demo-无血缘关系进程通信:
1、创建管道
2、写一个写管道进程 .c
//fifo-w.c int main(int argc, char* argv[]) { if (argc < 2) { printf("format: ./a.out fifoname\n"); exit(1); } int fd = open(argv[1], O_WRONLY); if (fd < 0) { perr_exit("open error"); } char buf[128]; memset(buf, 0, sizeof(buf)); int i = 0; while (1) { sprintf(buf, "hello, world: %d\n", i++); write(fd, buf, strlen(buf)); sleep(1); } close(fd); return 0; }
3、写一个读管道进程 .c
//fifo-r.c int main(int argc, char* argv[]) { if (argc < 2) { printf("format: ./a.out fifoname\n"); exit(1); } int fd = open(argv[1], O_RDONLY); if (fd < 0) { perr_exit("open error"); } char buf[128]; while (1) { read(fd, buf, sizeof(buf)); printf("%s", buf); } close(fd); return 0; }
验证:
开两个终端分别执行两个不相关进程,发现读进程能够正常在屏幕上循环打印数据。这中间就是内核中的 FIFO 在起桥梁通信作用。
FIFO也允许多写一读。
进程间通信2--文件(淘汰)
- <文件>没有阻塞特性,read不会发生阻塞。读写时序不容易把控。
- 没有血缘关系的进程也可以用文件进行进程间通信。
和共享内存的区别?
共享内存映射存在的意义?和IO操作文件的关系?
进程间通信3--共享内存mmap(进程间通信速度最快、多次读取)
mmap-内存映射原理
mmap 即 memory map,也就是内存映射。mmap 是一种内存映射文件的方法,即将磁盘上一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存而直接改变磁盘上文件内容。而不需要频繁由用户进程通过内核,调用系统IO进行反复读写。
————————————————讲一讲什么是 MMAP_mmap是什么_Young丶的博客-CSDN博客
其实背后的逻辑没这么简单,更多参考:
Linux中进程间的通信方式--内存映射区mmap()_mmap 映射做 进程间同步,延时多大-CSDN博客mmap原理简析_51CTO博客_mmap原理
创建内存映射:
void* mmap(void* addr, size_t length, int prot, int flags,int fd, off_t offset); //创建映射区 //参数: addr:指定映射区的首地址, 通常传NULL, 表示让系统自动分配 length:共享内存映射区大小(<=文件的实际大小) prot:共享内存映射区的读写属性, PROT_READ, PROT_WRITE及PROT_READ|PROT_WRITE flags:标注共享内存的共享属性, MAP_SHARED或MAP_PRIVATE(shared内存的变化会反映到文件上, private不会反映到文件上) fd:用于创建共享内存映射区的那个文件描述符 offset:偏移位置, 需是4k的整数倍. 默认0, 表示映射文件全部 //返回值: 成功返回映射区首地址 失败返回MAP_FAILED(宏的值是:(void*)-1 ) , 设置errno
Demo:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<errno.h> #include<pthread.h> //使用mmap函数建立映射区 int main(int argc, char* argv[]) { int fd = open("mmaptext", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perr_exit("open error"); } //直接open创建文件大小的方式,创建文件大小是0。因此需要扩展文件大小到20B int ret = ftruncate(fd, 20); if (ret < 0) { perr_exit("ftruncate error"); } //获取文件大小 off_t len = lseek(fd, 0, SEEK_END); //len通常是文件大小,但一定小于等于文件大小 char* p = mmap(NULL, len, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0); if (p == MAP_FAILED) { //映射失败 perr_exit("mmap error"); } //接下来,p就相当于fd。我们可以用丰富的c库函数去直接操作文件。 //验证 strcpy(p, "hello, world\n");//相当于写操作 printf("%s", p);//验证也可以用vi打开文件看一下,是不是写进去了 munmap(p, len);//释放删除映射区 return 0; }
删除内存映射-munmap(类似内存的申请和释放函数):
int munmap(void* addr, size_t length); //删除映射区
MMAP使用注意事项:
8个注意问题?
会出现的问题:
注意问题对应的根因:
1.可以, 但是要拓展文件大小, 否则会出现总线错误. 当然, 如果破罐子破摔, mmap时指定size=0, mmap会报错
2.mmap会报错: 无效参数(mmap权限不能超过文件权限)。如果都一致用只读权限, 不会出错;另外mmap要创建映射区, 内部逻辑会读文件,所以文件本身必须有读权限。
3.没有影响, 建立完映射区后fd就能关闭。
4.mmap报错: 无效参数, 偏移量必须是4k的整数倍(因为MMU帮忙映射的最小单位就是4k)
5.小范围的越界问题不大, 但是最好不要这么做(操纵不安全的内存, 操作系统不给你保障)
6.不能成功. 与malloc一样, 释放的内存的指针必须是申请得来的初始的指针, 如果要改变指针的值, 拷贝一份用
7.除了第一个参数, 后面的参数都可能导致失败
8.会死的很难看
————————————————
原文链接:https://blog.csdn.net/DanielSYC/article/details/118458055mmap用法注意总结:
基于上面问题,MMAP的最保险的调用方式:
fd=open("filename",O_RDWR);
char* p = mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAX_SHARED,fd,0);
if (p == MAP_FAILED) { //映射失败
perr_exit("mmap error");
}
父子进程之间通信demo:
//mmap-fork.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/wait.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<error.h>
void perr_exit(const char *s){
perror(s);
exit(1);
}
int main(int argc, char* argv[]) {
//建立有大小的文件、创建映射、拿到映射指针
int fd = open("temp", O_RDWR | O_TRUNC | O_CREAT, 0644);
if (fd < 0) {
perr_exit("open error");
}
ftruncate(fd, 4);//记得扩展文件
int* p = (int*)mmap(NULL, fd, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);//MAP_SHARED
if (p == MAP_FAILED) {
perr_exit("mmap error");
}
close(fd);//映射完就可以关了,后面用指针操作就可以了
//创建子进程
int var = 100;
pid_t pid = fork();
if (pid < 0) {
perr_exit("fork error");
} else if (pid == 0) { //父子进程通信--依赖同一指针
printf("child before write: *p = %d, var = %d\n", *p, var);
*p = 9527;//子进程写共享内存
var = 200;
printf("child after write: *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
wait(NULL);
printf("parent: *p = %d, var = %d\n", *p, var);//父进程读共享内存
munmap(p, 4);
}
return 0;
}
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,修改不反应到磁盘实际文件,即私人的“写\时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
验证:对比:mmap使用shared参数;mmap改为使用private参数
前者 子写父读 没问题,能正常通信;后者子能正常写(应该写到子进程mmap的拷贝区了),父没读出来
*重点demo*无血缘关系进程之间通信demo:
写进程
int main(int argc, char* argv[]) {
struct Student stu = {1, "daniel", 22};
//创建文件并映射文件
int fd = open("temp", O_RDWR | O_TRUNC | O_CREAT, 0644);
if (fd < 0) {
perr_exit("open error");
}
ftruncate(fd, sizeof(stu));//内存映射和内存申请函数使用方法真的很像
struct Student* ps = (struct Student*)mmap(NULL, sizeof(stu), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ps == MAP_FAILED) {
perr_exit("mmap error");
}
//不断刷新文件的内容
while (1) {
memcpy(ps, &stu, sizeof(stu));
stu.id++;
sleep(1);
}
munmap(ps, sizeof(stu));
close(fd);
return 0;
}
读进程:
int main(int argc, char* argv[]) {
//打开已经有的文件
int fd = open("temp", O_RDONLY);
if (fd < 0) {
perr_exit("open error");
}
//依然将该文件映射
struct Student* ps = (struct Student*)mmap(NULL, sizeof(struct Student), PROT_READ, MAP_SHARED, fd, 0);
if (ps == MAP_FAILED) {
perr_exit("mmap error");
}
//不断读映射区内容
while (1) {
printf("stu id = %d, name = %s, age = %d\n", ps->id, ps->name, ps->age);
sleep(1);
}
munmap(ps, sizeof(struct Student));
close(fd);
return 0;
}
关键思路总结:无血缘关系进程间mmap通信
MMAP匿名映射区(有血缘关系进程):
匿名映射不创建文件,直接在有血缘关系进程之间共享P。因此,无血缘关系进程无法(通过第三方文件)使用匿名映射。
ps:
/dev/zero
-文件白洞, 里面有无限量的’\0’, 要多少有多少
/dev/null
-文件黑洞, 可以写入任意量的数据所以在创建映射区时可以用zero文件, 就不用自己创建文件然后拓展大小了
注意:/dev/zero
文件也不能用于无血缘关系进程间通信
Demo:
//mmap-anonymous.c
int main(int argc, char* argv[]) {
//直接获取一个映射区(利用 MAP_ANONYMOUS 参数)
int* p = (int*)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED)
perr_exit("mmap error");
//直接创建子进程 开始通信
pid_t pid = fork();
if (pid == -1)
perr_exit("fork error");
if (pid == 0) {
*p = 9527;
var = 200;
printf("I'm child,*p=%d,var=%d\n", *p, var);
} else if (pid > 0) {
sleep(1);
printf("I'm parent,*p=%d,var=%d\n", *p, var);
wait(NULL);
munmap(p, 4);
}
return 0;
}
进程间通信4--信号
信号基本概念(特性、产生、状态、处理)
信号的概念:
信号是 Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断.它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
信号的特性:
- 简单
- 少信息
- 满足特定条件才能发送
信号的特质:
- 软中断
- 由内核负责产生、收发
- 必须被立即处理
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
信号的产生常见例子(最终都是驱使内核产生):
- 按键产生:Ctrl+c, Ctrl+z, Ctrl+\
- 系统调用产生:kill, raise, abort
- 软件条件产生:定时器alarm
- 硬件异常产生:非法访问内存(段错误), 除0(浮点数例外), 内存对齐错误(总线错误)
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被О除,或者引用了无法访问的内存区域。
- 命令产生:kill命令
信号的状态:
递达:内核发出的信号递送并且到达进程;
未决:未递达或者递达了未被处理
信号的处理方式:
- 执行默认动作(每一个信号都有自己的默认处理方式)
- 丢弃(忽略)
- 捕捉(回调用户处理函数)(用户自定义信号到达之后的处理方式)
信号四要素: 编号,名称,触发事件,默认处理动作
常见信号:
常规信号一览: $ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
注:前31个位常规信号, 有默认事件和处理动作。后面的是实时信号(对实时性有要求的领域,底层驱动开发时会用到。应用开发只会用前31个), 没有默认事件和处理动作。
ubuntu:
常用信号 编号-名称-触发事件-默认处理动作:
1) 操作系统留出的唯一无条件终止、暂停进程、不允许被捕捉的信号,9/19:
SIGKILL(9)
和SIGSTOP(19)
, 不允许忽略和捕捉, 只能执行默认动作。综上所述,信号的默认处理动作大概分类:
- Term: 终止进程
- Ign: 忽略信号(默认即时对该种信号忽略操作)
- Core: 终止进程, 生成Core文件(查验进程死亡原因, 用于gdb调试)
- Stop: 暂停进程
- Cont: 继续运行进程
可以发送信号的系统函数
kill
一个弑父的例子:
//注意函数原型,kill本质是发信号的函数,而不直接是一个杀死进程的函数。
int kill(pid_t pid, int sig); //send signal to a process
//kill-demo.c
int main(int argc, char* argv[]) {
pid_t pid = fork();
if (pid < 0) {
perr_exit("fork error");
} else if (pid == 0) {
sleep(1);
kill(getppid(), SIGKILL);
} else {
while (1) {
printf("I'm parent\n"); //疯狂输出
}
}
return 0;
}
关于kill杀死进程pid参数,pid的不同取值(重点前三个):
kill -9 -10698 :
杀死10698进程组的所有进程
关于发送权限:发送者实际有效的用户ID==接收者实际有效的用户ID。
alarm
unsigned int alarm(unsigned int seconds);
demo:在定时器时间下,测试一秒钟计算机能数多少个数:
int main(int argc, char* argv[]) { int i = 0; alarm(1); while (1) { printf("i = %d\n", i); i++; } return 0; }
顺便使用
time
命令查看程序执行时间占用情况:
输出:
分析:
原理上,程序实际执行时间=系统内核时间+用户态时间+等待(系统资源:cpu、内存、标准输出IO资源)时间。
这个程序数的数少,大部分时间在等待使用标准输出。验证(不使用标准输出资源):
setitimer
int setitimer(int which, const struct itimerval* new_value, struct itimerval* old_value);
//成功返回0, 失败返回-1并设置errno
//参1which指定定时方式以及 ->发送的信号:
自然定时:ITIMER_REAL ->SIGALRM
用户空间计时(只计算进程占用CPU的时间):ITIMER_VIRTUAL ->SIGVTALARM
运行时计时(用户+内核):ITIMER_PROF ->SIGPROF
//参2是传入参数,设单次和周期定时时间;
//参3是传出参数,上次定时剩余的时间;
it_interval:设定周期定时的时间间隔
it_value:设定单次定时时长(如果都设置,触发逻辑是do while结构。等it_value秒后触发闹钟, 以后每隔it_interval触发一次)
struct itimerval {
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
/*精确到us的时间结构体*/
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
Demo:
#include<stdio.h>
#include<sys/time.h>
#include<signal.h>
void myfunc(int signo) {
printf("hello, world\n");
return;
}
int main(int argc, char* argv[]) {
//为SIGALRM注册回调函数
signal(SIGALRM, myfunc);//设置捕捉信号后的执行动作
// 5s后触发一次;随后每隔1s周期性触发一次
struct itimerval it = {{1, 0}, {5, 0}};
struct itimerval oldit;
int ret = setitimer(ITIMER_REAL, &it, &oldit);
if (ret < 0) {
perr_exit("setitimer error");
}
while (1);//保持进程,啥也不干
return 0;
}
信号集操作函数(阻塞信号集、未决信号集)
阻塞信号集是当前进程要阻塞处理的信号的集合,未决信号集是当前进程中还处于未决状态的信号的集合,这两个集合存储在内核的PCB中。
未决信号集-位图-(用来标识信号的当前状态0/1):
当有信号传递到该进程的时候,未决信号集的对应位立刻设置为1,其他位不变,这个时候信号只是传递到进程,并未被处理,叫作未决状态。
阻塞信号集(信号屏蔽字)-位图 - (用来标识信号的主动管理):
未决信号想要递达程序的信号处理函数(默认、忽略、自定义),还要经过信号屏蔽字的过滤。
如果事先将某些信号加入集合, 对他们设置屏蔽, 收到该信号时该信号的处理将推后,暂不处理(直到解除屏蔽后)。注意, 屏蔽信号, 只是将信号处理延后执行(延至解除屏蔽);而忽略表示将该信号丢弃处理。
如下图:PCB中有两个信号集(位图),未决信号集表状态,不能操作;阻塞信号集,可以操作,人为对进程某个信号进行屏蔽,暂不处理(默认全不阻塞 0)。
下面以SIGINT为例说明信号未决信号集和阻塞信号集的关系:
当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:
如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集 上该位置上的值保持为1,表示该信号处于未决状态;
如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。
当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。
————————————————
原文链接:https://blog.csdn.net/m0_60663280/article/details/121461762
屏蔽信号集以及其用户操作函数:
/*自定义信号集*/
sigset_t set;//“set”英文 集合
/*全部清空*/
int sigemptyset(sigset_t* set);
/*全部置1*/
int sigfillset(sigset_t* set);
/*将一个信号添加到集合当中(置1)*/
int sigaddset(sigset_t* set, int signum);
/*将一个信号从集合中移除 (置0)*/
int sigdelset(sigset_t* set, int signum);
/*判断某一信号是否在集合当中(为1)*/
int sigismember(const sigset_t* set, int signum);
//返回值: 成功返回0, 失败返回-1并设置errno;
屏蔽信号集如何生效至PCB中的阻塞信号集-sigprocmask
我们一般想要屏蔽或解除屏蔽PCB中的某个信号,我们不能直接修改PCB中屏蔽信号集,只能通过用户的set加上 sigprocmask函数,通过与PCB的Set进行位操作(这个过程不能影响其他信号),间接修改内核进程PCB中的信号屏蔽字。
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
//入参1 how:
SIG_BLOCK:设置阻塞, set表示需要屏蔽的信号; 新老 位或 操作
SIG_UNBLOCK:设置非阻塞, set表示需要解除屏蔽的信号;新取反 与老 位与 操作
SIG_SETMASK:用set替换原始屏蔽集;//有可能影响其他信号,一般不用
//入参2:
set:传入参数, 是一个位图, set中哪位置1, 就表示当前进程屏蔽哪个信号
//入参3:
oldset:传出参数, 保存旧的信号屏蔽集
读取当前进程的未决信号集函数:
传出后进一步查看二进制位
int sigpending(sigset_t* set);
//set是传出参数;
//返回值: 成功返回0, 失败返回-1并设置errno;
Demo: Ctrl+D 是向终端中写入一个 信号:
先设置当前进程的 某个信号阻塞,然后发送该信号,验证是否能真的没动作,也可以进一步读取未决信号集,验证这个信号正在被阻塞。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
//sigset-demo.c
void print_sigset(sigset_t* set) {
for (int i = 1; i < 32; ++i) {
if (sigismember(set, i)) {
printf("1");
} else {
printf("0");
}
}
printf("\n");
}
int main(int argc, char* argv[]) {
sigset_t new_sigset, old_sigset, ped_sigset;
//定义好用户的set,要屏蔽哪些信号
sigemptyset(&new_sigset);
sigaddset(&new_sigset, SIGQUIT); //屏蔽Ctrl+'\'
sigaddset(&new_sigset, SIGKILL); //屏蔽SIGKILL,但是无效
//让阻塞信号集实际生效
sigprocmask(SIG_BLOCK, &new_sigset, &old_sigset);
//死循环,手动按键产生信号和读取未决信号集验证信号确实是被阻塞
while (1) {
int ret = sigpending(&ped_sigset); //读取未决信号集
if (ret == -1) {
perr_exit("sigpending error");
}
print_sigset(&ped_sigset);
sleep(1);
}
return 0;
}
注意: 对于SIGKILL
信号, 即使设置了信号屏蔽, 依然能kill。可以查询当前进程的进程号之后,使用kill干掉当前进程。
**信号捕捉**
signal
//该函数由ANSI定义, 由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为, 因此应尽量避免使用它, 取而代之使用
sigaction
函数;/*定义回调函数类型,很不幸,函数类型限制死了*/ typedef void (*sighandler_t)(int); /*注册信号捕捉函数*/ sighandler_t signal(int signum, sighandler_t handler);
//signal-demo.c - 一个打印信号是几号编号的demo // 信号捕捉函数/回调函数:调用者是内核 void func(int signum) { //signum是信号编号;这个是信号捕捉函数的函数定义 printf("catch you %d\n", signum); } int main(int argc, char* argv[]) { //**注册** 信号捕捉函数 signal(SIGINT, func); while (1) ; return 0; }
这里,强化一下回调函数和函数指针
什么函数才是回调函数?
*sigaction函数*
//oldact 传出参数
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
struct sigaction {
void (*sa_handler)(int);//捕捉动作函数:返回值为空;函数名;函数参数为int
void (*sa_sigaction)(int, siginfo_t *, void *); //不用
sigset_t sa_mask; //只工作于信号捕捉函数执行期间,相当于中断屏蔽
int sa_flags; //本信号默认屏蔽
void (*sa_restorer)(void); //废弃
};
demo:
void catch_signal(int signum) {
if (signum == SIGINT) {
printf("catch SIGINT\n");
} else if (signum == SIGQUIT) {
printf("catch SIGQUIT\n");
}
}
int main(int argc, char* argv[]) {
struct sigaction act, oldact;
act.sa_handler = catch_signal;
act.sa_flags = 0;//本信号默认屏蔽
sigemptyset(&act.sa_mask);//全空,全不屏蔽
sigaction(SIGINT, &act, &oldact);
sigaction(SIGQUIT, &act, &oldact);
while (1)
;
return 0;
}
信号捕捉的特性:
- 捕捉函数执行生命周期期间, 信号屏蔽字由
mask
变为sigaction
结构体中的sa_mask实际生效
, 捕捉函数执行结束后, 恢复回mask。
- (因为信号的接受执行优先级高于正在执行的信号捕捉函数)捕捉函数执行期间, 本信号自动被屏蔽
(sa_flags=0)。保证进程不总在“接受同一新信号,而没机会处理信号执行函数”
- 并不是每次捕捉信号都会响应执行一次。没有排队积攒机制。捕捉函数执行期间, 若被屏蔽信号多次发送, 解除屏蔽后只响应一次
内核信号捕捉过程简析:
解析过程:
信号处理-1进入内核:
可能有短暂的延时体现在:信号发了,相应的进程不一定会立即感知和处理信号,必须因为一些时机进入到内核了(比如代码涉及了系统调用);
信号处理-2 顺便查看当前进程,PCB中是否未决信号待处理;并查看信号屏蔽字,该信号是不是需要进一步递送处理;
信号处理-3 分类响应需要处理的信号。如果是捕捉信号,则内核调用对应的信号捕捉函数。
信号处理-4 执行完信号处理函数后要再次进入内核
因为信号处理函数是内核调用的, 函数执行完毕后要返回给调用者 CSDN
https://mp.csdn.net/mp_blog/creation/editor?spm=1000.2115.3001.5352
信号处理-5 从内核返回用户进程中断往下执行。
sigchild信号-借助信号捕捉回收子进程
背景:在有些时候父进程是没有办法设置回收子进程的。比如exec函数,exec函数成功运行就不会再返回,就只能你通过系统的隐式回收来达到回收目的。但通过信号可以实现:子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
第一版:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<errno.h> #include<signal.h> #include<sys/wait.h> void sys_err(const char *str) { perror(str); exit(1); } //创建捕捉函数 void catch_child(int signo) { pid_t wpid; wpid = wait(NULL); if(wpid == -1) sys_err("wait error"); printf("catch child id %d\n", wpid); return; } int main(int argc, char *argv[]) { //⭐循环创建多个子进程⭐ pid_t pid; int i; for(i = 0; i<5; ++i) { if((pid = fork()) == 0) break; } if(i == 5) { struct sigaction act; act.st_handler = catch_child; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGCHLD, &act, NULL); printf("I am parent = %d\n", getpid()); }else { printf("I am child = %d\n", getpid()); } return 0; }
但是上述代码会出现多个子进程同时死亡,即同时发送信号,而信号是不排队的,再阻塞后只能处理一个信号即回收一个子进程,这样就会出现僵尸现象。(僵尸:复习就是子进程死了父进程没给回收)
*****解决方法:
在回调函数中,用循环实现一次回调多次回收。void catch_child(in signo) { pid_t wpid; while(wpid = wait(NULL) != -1) { printf("-------catch child id = %d\n",wpid); } return; }
慢速系统调用中断:
进程间通信5--消息队列
进程间通信6--本地套接字
第3篇 进程组与线程
进程组与会话
Linux下的进程本身都是以init为祖先进程的一个树状进程族谱,Init进程就是这个树的根。但是为方便管理有其他层级关系进程们,又在简单的父子关系之外增加了进程组和会话的关系,从而方便进程的管理。当一个用户登录到系统时登录程序就会将登录的shell设置成一个会话首领和组长进程。简单来说是这样的一种关系 会话 > 包括进程组 > 包括进程 。而增加这样的额外关系仅仅是为了方便管理控制所有由关系的进程(作业)。在Linux中一个进程除了是属于他父进程的子进程外他还是一个进程组的成员,而同一进程组有这样一个特点-----信号会被传递到同一进程组的所有进程。
————————————————
原文链接:https://blog.csdn.net/m0_74282605/article/details/132918949
进程组
进程组,也叫做作业。一个或者多个进程的集合,每个进程都属于一个进程组。操作系统设计进程组的概念主要就是为了简化对多个进程的管理。
- 当父进程创建子进程的时候,默认父进程和子进程同属于同一个进程组,进程组的ID就是组长进程的ID。
- 只要进程组中有一个进程存在,那么这个进程组就存在,并且即使组长进程终止也不会影响进程组。进程组的生命周期时从进程组创建到进程组最后一个进程终止或转移到另一个进程组。
- 一次性杀死整个进程组的方式:kill -9 -(进程组IO)
会话
会话:进程组的上一级,多个进程组对应一个会话。
创建会话的注意事项:
- 调用进程不能是进程组组长(父进程不能创建会话)(即为普通组员进程),该进程变成新会话首进程以及session header;如果创建会话的进程是进程是组长进程,则出错返回,也就是说组长不能当会长;
- 该进程成为一个新的进程组的组长进程,也就是说如果子进程创建了一个会话,那么子进程就脱离父进程的进程组,成为一个新进程的进程组组长;
- 新会话丢弃原有的控制终端,该会话没有控制终端(没有交互命令窗口;适合在后台运行);
- 建立新会话时,先调用fork,父进程终止,子进程调用setsid,也就是说,只有父进程终止了,子进程才能创建会话;
demo1:
下图从左至右:父进程ID 进程ID 进程组ID 会话ID
demo2:在bash连续cat 加管道
分析:几个cat进程属于 同一会话、进程组 父进程ID
创建会话及获得会话ID getsid/setsid
进程
会话创建成功后,三ID合一,PID、组ID、会话ID
例如:这个会话的会长是bash
#include <unistd.h>
pid_t getsid(pid_t pid);
pid_t setsid(void);
创建会话demo:
体会:建立新会话时,先调用fork,父进程终止,子进程调用setsid,也就是说,只有父进程终止了,子进程才能创建会话;
守护进程
创建守护进程一般步骤:
- 创建子进程, 父进程退出: 所有工作在子进程中形式上脱离了控制终端
- 在子进程中创建新会话: setsid()函数, 使子进程完全独立出来, 脱离控制
- 改变当前进程工作目录位置: chdir()函数, 可执行文件工作目录 放到不可卸载/改变的工作目录文件系统下。
- 重设文件权限掩码: umask()函数, 防止继承的文件创建屏蔽字拒绝某些权限
- 关闭/重定向文件描述符: (0 1 2)继承的打开文件不会用到, 浪费系统资源, 无法卸载
- 开始执行守护进程核心工作(有一个大循环)
————————————————
原文链接:https://blog.csdn.net/DanielSYC/article/details/118458055
***守护进程创建 demo***:
//deamon.c
int main(int argc, char* argv[]) {
//创建子进程,关闭父进程
pid_t pid = fork();
if (pid != 0) {
exit(0);
}
//创建新会话
int ret = setsid();
//切换工作目录,防止当前目录被卸载
chdir("/home/daniel");
umask(0022);
//下面这几行的目的是让null的fd占住0,也让标准输出和错误重定向
close(STDIN_FILENO); //关闭标准输入
int fd = open("/dev/null", O_RDWR);
if (fd < 0) {
perr_exit("open error");
}
//将标准输出和标准错误重定向到/dev/null
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
while (1)
;
return 0;
}
注销用户,重新登陆,不受影响。
线程
线程概念/特性
- 又叫LWP(轻量级进程)
- 进程,有独立的地址空间,有PCB;线程,有独立的PCB,但是没有独立的地址空间(共享),所以二者区别就在于是否共享地址空间。
- 线程是最小的执行单位;进程是最小分配资源的单位
![]()
![]()
- ps -Lf pid:查看一个进程开的线程个数。
linux内核线程实现原理
线程之间共享的资源:
- 文件描述符表 (线程间通信无障碍)
- 到来的信号(谁都可以抢)
- 当前工作目录位置
- 用户ID和组ID
- 内存地址空间(.text/.data/.bss(全局变量)/.heap/共享库) (唯独没有栈)
线程非共享资源:
- 线程id
- 处理器现场(寄存器的值)和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量
- 信号屏蔽字
- 调度优先级
线程优缺点:
pthread_self(void)
获得线程ID
pthread_t pthread_self(void);
注意: 和LWP(线程号/(接近于进程号),用来给cpu时间调度用)概念区别开。
pthread_create()
创建线程
int pthread_create(pthread_t* thread,const pthread_attr_t* attr,void* (*start_routine)(void* ),void* arg);
入参:
1、传出参数,调用成功,传出新创建线程的线程ID
2、设置线程属性,可为null
3、回调函数指针。一旦子线程产生,新产生的子线程会默认去执行回调函数
4、参3 回调函数的参数,void*类型
返回值:
成功,返回0;
错误,返回错误号,并且1参无值
线程创建 demo:
//pthread_create-demo.c
void* tfn(void* arg) {
printf("tfn:pid=%d,tid=%lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char* argv[]) {
//printf("main:pid=%d,tid=%lu\n", getpid(), pthread_self());
pthread_t tid = 0;
//子线程一旦创建成功,就去自动执行其回调函数
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0)
perr_exit("pthread_create error");
/*父进程(主线程)等待1秒,否则父进程(主线程)一旦退出,地址空间被释放,子线程回调函数没机会执行*/
sleep(1);
return 0;
}
***循环创建多个子线程 demo***:
//pthreads.c
void* tfn(void* arg) {
long i = (long)arg;
sleep(i);
printf("I'm %ld thread, pid = %d, tid = %lu\n", i, getpid(), pthread_self());
return NULL;
}
int main(int argc, char* argv[]) {
for (long i = 0; i < 5; ++i) {
pthread_t tid;
//注意参4传递方式, 先将int型的i强转成void*传入, 用到时再强转回int型
int ret = pthread_create(&tid, NULL, tfn, (void*)i);
if (ret < 0) {
perr_exit("pthread_create error");
}
}
sleep(5);
return 0;
}
CSDNhttps://mp.csdn.net/mp_blog/creation/success/134006567
写多线程代码gcc编译时候,注意加 -lpthread 参数
循环创建多个子线程 demo error版:
注意正确版本参数4传递方式:先将int
型的循环变量序号i
强存在 (void*
) 指针类型的内存中值
传入, 用到时再强转回int
型
对照一个有错误的版本:如果不用强转, 看似规规矩矩的传地址再解引用, 会出现问题
/*这是一个出错的版本*/
void* tfn(void* arg){
int i=*((int*)arg);
printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
sleep(i);
return NULL;
}
int main(int argc,char* argv[]){
int i=0;
int ret=0;
pthread_t tid=0;
for(i=0;i<5;++i){
ret=pthread_create(&tid,NULL,tfn,(void*)&i);//参4传递存在问题
if(ret!=0)
perr_exit("pthread_create error");
}
sleep(i);
return 0;
}
解引用出来的循环变量i值不是期望情况:
根因:
main中给tfn传入的是他的函数栈帧中局部变量i的地址,这样tfn能根据地址直接访问到i的值。考虑到线程之间是并发执行的,在第i轮循环中,创建了第i个线程,紧接着主线程继续执行下一轮循环,i自增;同一时间,i线程开始执行回调函数,从mian主线程中取局部变量i的数据。此时可能i值已经变化了,访问到的值是很随机的。
使用强转可以保证变量i的实时性(C语言值传递的特性),而不是传过去地址再回头带着地址来取值。
ps注意:线程A不能直接访问线程B的栈数据。因为线程栈是线程私有的,只有拥有该线程的线程才能访问该线程的栈数据。但是,线程A可以通过线程B暴露栈地址的方式来访问线程B的栈数据。另外,线程A可以访问进程中所有线程共享的变量和堆内存。
————————————————
原文链接:https://blog.csdn.net/DanielSYC/article/details/118458055
pthread_exit(void* retval)
退出当前线程
void pthread_exit(void* retval);
//retval表示传出退出值,无退出值时,通常传NULL
概念区别:
exit()
函数用来退出当前进程,如果用在线程中,直接把整个进程即所有线程都退出了;
pthread_exit()
函数才是用来将单个的线程退出;
return是退出当前函数,也可以起到退出线程或者进程的效果,主要看放在那里。
有了线程退出的概念,“线程创建 demo”中主线程必须睡一秒,子线程才来得及打印的问题就能换个方式解决了。如下:只退出主线程,不影响子线程执行。
//pthread_exit-demo.c
void* tfn(void* arg) {
long i = (long)arg;
if (i == 2)
pthread_exit(NULL);
printf("I'm %ld thread,pid=%d,tid=%lu\n", i, getpid(), pthread_self());
sleep(i);
return NULL;
}
pthread_join()
pthread_join()函数,以阻塞的方式等待thread指定的线程结束,获取线程的退出值。当函数返回时,被等待线程的资源被收回。如果指定线程已经结束,那么该函数会立即回收资源返回。
和进程的 wait()一个作用,阻塞等待回收。
int pthread_join(pthread_t thread, void** retval);
//成功返回0,失败返回errno
//参1:指定线程ID;
//参2:传出线程退出状态void**(线程的退出状态(返回值)是void*,回收时传的就是void**)
回收指定线程demo:
//pthread_join-demo.c
struct thrd {
int var;
char str[256];
};
void* tfn(void* arg) {
struct thrd* pt = (struct thrd*)malloc(sizeof(struct thrd));
pt->var = 9527;
strcpy(pt->str, "hello, world");
return (void*)pt;
}
int main(int argc, char* argv[]) {
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
struct thrd* pt;
pthread_join(tid, (void**)&pt);
printf("thread returns pt->var = %d, pt->str = %s\n", pt->var, pt->str);
free(pt);
return 0;
}
注意:pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者malloc分配的, 不能在线程函数的栈上分配, 因为其他线程得到这个返回指针时线程函数已经退出了。回收会收不到。
pthread_detach()
将线程设置为分离状态
效果:线程运行结束后,不用再调用join手动回收了,线程终止会自动回收清理pcb。
int pthread_detach(pthread_t thread);
//pthread_detach-demo
void* tfn(void* arg) {
printf("tfn:pid=%d,tid=%lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char* argv[]) {
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
pthread_detach(tid); //设置线程分离
sleep(1);
int ret = pthread_join(tid, NULL); //这里会出错,不能对一个已经分离出去的子线程回收
if (ret != 0) {
printf("pthrad_join error: %s\n", strerror(ret));
exit(1);
}
return 0;
}
pthread_cancel()
类似于kill()
,用于杀死线程。
int pthread_cancel(pthread_t thread);
//成功,返回0;失败,返回错误号
//pthread_cancel-demo.c
void* tfn(void* arg) {
while (1) {
printf("pid = %d,tid = %lu\n", getpid(), pthread_self());
sleep(1);
}
return NULL;
}
int main(int argc, char* argv[]) {
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
//等待5s后杀死该线程
sleep(5);
int ret = pthread_cancel(tid);
if (ret != 0) {
perr_exit("pthread_cancel error", ret);
}
return 0;
}
注意下面情况:当回调函数没有进内核 的代码,则pthread_cancel无法杀死线程。
cancel
必须要等待取消点/保存点(进入内核的契机),所以如果一个线程一直不使用系统调用(一直不进内核),cancel
就无法杀死该线程。可以手动添加一个取消点pthread_testcancel()。
Linux下线程有三种结束的方法:
1. 线程函数执行完毕,线程正常结束;
2. 线程调用pthread_exit(void* rval_ptr)
3. 线程被取消(也就是其他线程调用pthread_cancel(pthread_t)
//pthread_cancel-endof3.c
void* tfn1(void* arg) {
printf("thread1: returning\n");
return (void*)111;
}
void* tfn2(void* arg) {
printf("thread2: exiting\n");
pthread_exit((void*)222);
}
void* tfn3(void* arg) {
while(1) {
//下面的两条语句都会陷入内核,从而处理calcel“信号”,杀死线程
printf("thread3: going to die in 3 seconds\n");
sleep(1);
//pthread_testcancel(); //手动添加取消点
}
return (void*)666;
}
int main() {
pthread_t tid;
void* ret;
//回调函数return
pthread_create(&tid, NULL, tfn1, NULL);
pthread_join(tid, &ret);
printf("thread1 exit code = %ld\n", (long)ret);
//使用线程退出函数
pthread_create(&tid, NULL, tfn2, NULL);
pthread_join(tid, &ret);
printf("thread2 exit code = %ld\n", (long)ret);
pthread_create(&tid, NULL, tfn3, NULL);
sleep(3);
pthread_cancel(tid);
//线程被杀死,非正常执行终止,也需要回收。回收的子线程状态为-1,表示子线程是非正常死亡的。
pthread_join(tid, &ret);
printf("thread3 exit code = %ld\n", (long)ret);
return 0;
}
进程和线程控制原语对比
注意:进程回收只能父进程回收子进程;线程回收只要调了回收函数。
线程属性(pthread_create 二参)
先本地创建和初始化线程属性结构体,中间赋值需要的成员属性(这里是分离状态属性),再pthread_create
创建线程,最后记得销毁线程属性结构体。
/*初始化线程属性:成功返回0,失败返回errno*/
int pthread_attr_init(pthread_attr_t* attr);
/*销毁线程属性所占用的资源:成功返回0,失败返回errno*/
int pthread_attr_destroy(pthread_attr_t* attr);
线程的分离状态属性:
线程的分离状态决定一个线程以什么样的方式来终止自己
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当 pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
设置线程分离状态接口函数:
/*设置线程属性:分离或非分离*/
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);
/*获取线程属性*/
int pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate);
//detachstate取值:PTHREAD_CREATE_DETACHED 或 PTHREAD_CREATE_JOINABLE
下面直接看demo:
//pthread_attr_t-demo.c
void perr_exit(const char* str, int ret) {
fprintf(stderr, "%s:%s\n", str, strerror(ret));
pthread_exit(NULL); //为了不至于使子线程退出,主线程应调用pthread_exit()而非exit()
}
void* tfn(void* arg) {
while (1) {
printf("pid = %d,tid = %lu\n", getpid(), pthread_self());
sleep(1);
}
return NULL;
}
int main(int argc, char* argv[]) {
//先本地创建和初始化线程属性结构体
pthread_attr_t attr;
pthread_attr_init(&attr);
//利用分离属性接口赋值线程属性结构体
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//创建带有分离属性的线程,注意2参传参
pthread_t tid;
pthread_create(&tid, &attr, tfn, NULL);
//验证:
int ret = pthread_join(tid, NULL); //尝试回收,但是会失败,因为前面已经设置了线程分离属性
//销毁线程属性结构体
pthread_attr_destroy(&attr);
if (ret != 0) {
perr_exit("pthread_join error", ret);
}
pthread_exit(NULL); //为了不至于使子线程退出,主线程应调用pthread_exit()而非exit()
}
线程使用注意事项:
4.
各个子线程会均分进程的栈空间,但是线程的栈空间大小是可以调整的。
线程同步(协调)
线程同步概念
有多线程并发,就要考虑同步。一个线程正在使用某资源时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据的一致性,不能调用该功能,避免产生与时间有关的错误。
线程同步策略:对线程的操作加锁,将对资源的访问变成互斥操作。
锁使用注意事项:
Linux提供的锁都具有建议性(下面的两种锁,互斥锁和读写锁,都是),不认同锁机制的线程可以直接访问资源。
互斥量锁pthread_mutex_t
使用锁的一般流程:
//mutex:互斥锁
pthread_mutex_t lock; //创建锁 ,数据类型本质上是个结构体
pthread_mutex_init(); //初始化
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//也可以静态初始化,效果一样
pthread_mutex_lock(); //加锁
visit(); //开始访问数据
pthread_mutex_unlock(); //解锁
pthread_mutex_destory(); //销毁锁
先来看一个C的关键字:
restrict
用来限定指针变量,被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。用在下面:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
*锁使用 demo:
//lock-demo.c
/*创建一把全局锁,这样子线程才能都能访问*/
pthread_mutex_t mutex;
void* tfn(void* arg) {
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex);
printf("hello, ");
sleep(rand() % 4);
printf("world\n");
pthread_mutex_unlock(&mutex);//立即解锁
sleep(rand() % 4);//这个sleep的目的是让主线程有机会获得cpu
}
return NULL;
}
int main(int argc, char* argv[]) {
//线程创建前,就把锁初始化好
pthread_mutex_init(&mutex, NULL);
srand(time(NULL));
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
pthread_mutex_lock(&mutex);//加锁
printf("HELLO, "); //干活
sleep(rand() % 4);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);//解锁
sleep(rand() % 4);
}
//进程结束前把锁销毁
pthread_mutex_destroy(&mutex);
pthread_join(tid, NULL);
return 0;
}
现象:
互斥锁使用技巧:
1、锁的粒度越小越好:访问前加锁,访问结束后立即解锁。不要占着茅坑***。
2、try锁(尝试加锁):try锁会尝试加锁,成功mutex--
,失败返回错误号;而lock如果加锁失败会阻塞,等待锁释放。
**死锁现象**
两种常见死锁:
- 同一个线程对一个
mutex
反复加锁,加第2次锁时,加不成功,阻塞。因为自己拿着锁,还没开。(自己把自己绊住了) - 线程1有A锁共享资源,请求B锁共享资源,才能完工;线程2有B锁,请求A锁,造成二者循环等待,(两个人互相掣肘了)
条件变量pthread_cont_t
条件变量概念
条件变量通常与互斥锁配合使用(wait函数参数之一),负责阻塞等待条件满足。
可以让当一个线程暂时访问不到共享资源的时候,时机成熟了有个机制能通知他。条件变量机制让锁的功能更强大。
条件变量主要应用函数
//condition
/*定义一个条件变量:静态初始化*/
//两种初始化条件变量的方法
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
int pthread_cond_init(&cond,NULL);//动态初始化条件变量
int pthread_cond_destory(&cond);
int pthread_cond_wait(&cond,&mutex);//阻塞等待一个条件满足,需要结合一把锁使用
//函数作用:
1、阻塞等待条件变量cond(参1)满足,并释放已经掌握的互斥锁,相当于pthread_mutex_unlock(&mutex)
2、当被唤醒,pthread_cond_wait()函数返回时,解除阻塞并重新申请互斥锁pthread_mutex_lock(&mutex)。
int pthread_cond_timewait();//限时等待,超时条件未来,则解除条件阻塞
int pthread_cond_signal(); //条件来了,通知(唤醒)(至少)一个线程
int pthread_cond_broadcast(); //条件来了,通知所有线程
关键函数解释:pthread_cond_wait (负责阻塞等待条件满足)
条件变量应用-生产者/消费者模型
生产者:
- 生产数据(饼)
- 加锁pthread_mutex_lock(&mutex)
- 将数据放置到公共区域(筐)
- 解锁pthread_mutex_unlock(&mutex)
- 通知阻塞在条件变量上的线程:pthread_cond_signal()或pthread_cond_broadcast()
- 循环生产后续数据
消费者:霸道的消费者来了看一眼筐(加锁),说待会饼熟了通知我我要来吃(设条件变量),说完离开(解锁)了(生产者抓紧在框里做饼中)。
过一会儿,饼熟了,消费者被通知到,来了把筐占住(加锁),开吃。
吃完,释放条件变量、释放锁。
- 创建锁pthread_mutex_t mutex
- 初始化pthread_mutex_init(&mutex,NULL)
- 给筐加锁pthread_mutex_lock(&mutex) 三连...
- 等待条件满足pthread_cond_wait(&cond,&mutex),首先解锁并阻塞等待条件变量,然后加锁。
- 访问共享数据
- 解锁,释放条件变量,释放锁
上面模型和下面两个博文是理解这个知识点的核心。
**条件变量使用demo**:
//producer-consumers.c
struct Node {
int val;
struct Node* next;
};
typedef struct Node Node;
Node* head = NULL;
//初始化互斥量锁 和 条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producted = PTHREAD_COND_INITIALIZER;
//消费者从头部开始往后吃链表新生成的结点
void* consumer(void* arg) {
while (1) {
//消费者线程想吃/删除公共区域的数据,先加锁
pthread_mutex_lock(&lock);
//然后阻塞等待条件成熟,此期间解锁
while (head == NULL) {//先看有没有饼,上来如果有饼就直接吃了;如果没饼,进入阻塞等待。
pthread_cond_wait(&has_producted, &lock);
}
Node* p = head;
head = head->next;
pthread_mutex_unlock(&lock);
printf("consume %d\n", p->val);
free(p);
sleep(rand() % 8);
}
return NULL;
}
//生产者使用头插法不断产生新的链表结点
void* producer(void* arg) {
while (1) {
Node* s = (Node*)malloc(sizeof(Node));
s->val = rand() % 1000;
printf("produce %d\n", s->val);
//生产者加锁向公共区域写数据,然后解锁
pthread_mutex_lock(&lock);
s->next = head;
head = s;
pthread_mutex_unlock(&lock);
//生产完数据发条件到达的信号
pthread_cond_signal(&has_producted);
//留出时间让消费者线程获得cpu
sleep(rand() % 2);
}
return NULL;
}
int main(int argc, char* argv[]) {
srand(time(NULL));
//创建一个生产者,3个消费者线程
pthread_t pid, cid[3];
pthread_create(&pid, NULL, producer, NULL);
for (int i = 0; i < 3; ++i) {
pthread_create(cid + i, NULL, consumer, NULL);
}
//线程运行结束,join等着回收
pthread_join(pid, NULL);
for (int i = 0; i < 3; ++i) {
pthread_join(cid[i], NULL);
}
return 0;
}
读写锁pthread_rwlock_t
读写锁概念
读写锁与互斥量类似,但读写锁允许更高的并行性。当读线程远大于写线程场景,读写锁相较于纯粹的互斥量锁,可以调高访问效率。其特性为:
- 写独占,读共享
- 写锁优先级高(读写锁一起过来时);读加锁成功,写锁就要等待解锁。
- 锁只有一把
读写锁只有一把,但其具备两种状态:
- 读模式下加锁状态(读锁)
- 写模式下加锁状态(写锁)
读写锁操作函数:
/*定义一个读写锁变量*/
pthread_rwlock_t rwlock;
int pthread_rwlock_init(&rwlock,NULL);
int pthread_rwlock_destory(&rwlock);
int pthread_rwlock_rdlock(&rwlock);
int pthread_rwlock_wrlock(&rwlock);
int pthread_rwlock_tryrdlock(&rwlock);
int pthread_rwlock_trywrlock(&rwlock);
int pthread_rwlock_unlock(&rwlock);
//都是成功返回0,失败直接返回错误号
读写锁 demo:
//rwlock-demo.c
int cnt = 0;
pthread_rwlock_t rwlock;
void* writer(void* arg) {
long i = (long)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
usleep(10000);
int t = cnt;
printf("I'm writer %ld, cnt = %d, ++cnt = %d\n", i, t, ++cnt);
pthread_rwlock_unlock(&rwlock);
usleep(100000);
}
return NULL;
}
void* reader(void* arg) {
long i = (long)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("I'm reader %ld, cnt = %d\n", i, cnt);
pthread_rwlock_unlock(&rwlock);
usleep(20000);
}
return NULL;
}
int main() {
pthread_rwlock_init(&rwlock, NULL);
pthread_t tid[8];
long i = 0;
for (; i < 3; ++i) {
pthread_create(tid + i, NULL, writer, (void*)i);
}
for (; i < 8; ++i) {
pthread_create(tid + 3 + i, NULL, reader, (void*)i);
}
for (i = 0; i < 8; ++i) {
pthread_join(tid[i], NULL);
}
pthread_rwlock_destroy(&rwlock);
}
I'm writer 0, cnt = 0, ++cnt = 1
I'm reader 3, cnt = 1
I'm reader 4, cnt = 1
I'm reader 5, cnt = 1
I'm reader 6, cnt = 1
I'm reader 7, cnt = 1
I'm writer 1, cnt = 1, ++cnt = 2
I'm writer 2, cnt = 2, ++cnt = 3
I'm reader 6, cnt = 3
I'm reader 3, cnt = 3
I'm reader 5, cnt = 3
I'm reader 4, cnt = 3
I'm reader 7, cnt = 3
I'm reader 3, cnt = 3
I'm reader 5, cnt = 3
I'm reader 6, cnt = 3
I'm reader 4, cnt = 3
I'm reader 7, cnt = 3
I'm reader 6, cnt = 3
I'm reader 5, cnt = 3
I'm reader 4, cnt = 3
I'm reader 3, cnt = 3
I'm reader 7, cnt = 3
I'm reader 4, cnt = 3
I'm reader 5, cnt = 3
I'm reader 3, cnt = 3
I'm reader 6, cnt = 3
I'm reader 7, cnt = 3
I'm writer 0, cnt = 3, ++cnt = 4
I'm reader 5, cnt = 4
I'm reader 3, cnt = 4
I'm reader 4, cnt = 4
I'm reader 6, cnt = 4
I'm reader 7, cnt = 4
I'm writer 1, cnt = 4, ++cnt = 5
I'm writer 2, cnt = 5, ++cnt = 6
I'm reader 5, cnt = 6
I'm reader 3, cnt = 6
I'm reader 4, cnt = 6
I'm reader 6, cnt = 6
I'm reader 7, cnt = 6
I'm reader 4, cnt = 6
I'm reader 5, cnt = 6
I'm reader 3, cnt = 6
信号量sem_t
信号量概念
和信号没有半毛钱关系
相当于初始化值为N的互斥量,N值表示可以同时访问共享区域的线程数。类似于操作系统理论课中的PV操作
信号量操作函数:
/*定义一个信号量*/ sem_t sem; /*信号量操作函数*/ int sem_init(sem_t* sem, int pshared, unsigned int value); int sem_destroy(sem_t* sem); int sem_wait(sem_t* sem);//加锁 int sem_trywait(sem_t* sem); int sem_timedwait(sem_t* sem, const struct timespec* abs_timeout); int sem_post(sem_t *sem);//解锁
- sem_wait():如果信号量>0,则信号量–;如果信号量=0,再加锁,就会造成线程阻塞
- sem_post():将信号量++,同时唤醒阻塞在信号量上的线程
- 当然,sem_t的实现对用户隐藏,所以所谓的++和--只能通过函数来实现,不能直接用++和--符号
- 信号量的初值,决定了占用信号量的线程个数
信号量实现的生产者消费者-demo:
//producer-consumer-sem_t.c
#define NUM 5
int arr[NUM];
sem_t empty, produce;//两个信号量,一个空格信号量,一个产品信号量
void* producer(void* arg) {
int i = 0;
while (1) {
int t = rand() % 1000;
printf("produce %d\n", t);
sem_wait(&empty);//生产者
arr[i] = t;
sem_post(&produce);//唤醒
i = (i + 1) % NUM;
sleep(rand() % 3);
}
return NULL;
}
void* consumer(void* arg) {
int i = 0;
while (1) {
sem_wait(&produce);//对产品加锁;最开始产品为0,阻塞。
printf("consume %d\n", arr[i]);
arr[i] = -1;
sem_post(&empty);
i = (i + 1) % NUM;
sleep(rand() % 3);
}
return NULL;
}
//信号量初始化
__attribute__((constructor)) void begin() {
sem_init(&empty, 0, NUM);//空格数 num
sem_init(&produce, 0, 0);//产品数 0
}
int main() {
srand(time(NULL));
pthread_t pid, cid;
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
__attribute__((destructor)) void end() {
sem_destroy(&empty);
sem_destroy(&produce);
}
待看: