首页 > 系统相关 >linux基本知识汇总2(系统编程) 60000字汇总

linux基本知识汇总2(系统编程) 60000字汇总

时间:2024-02-27 22:23:17浏览次数:22  
标签:-- pthread 汇总 --- int 60000 线程 linux 进程

/////////////进程/任务 -- task
任何启动并运行程序的行为,都是由操作系统帮助我们将程序转换成进程 -- 进程:完成特定的任务

进程控制块:PCB(win) / task_struct(linux) -- 结构体结点/内核数据结构 -- 提取了进程的所有属性


task_struct是PCB的一种
在Linux中描述进程的结构体叫做task_struct。task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

task_ struct内容分类
{
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。 I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
}

linux中所有运行在系统里的进程都以task_struct链表的形式存在内核里。

进程 = 进程控制块+内存中数据/代码
= 内核关于进程的相关数据结构+当前进程的代码和数据

进程管理:管理PCB

操作系统对进程的管理 == 描述+组织 == 对进程控制块的数据结构进行CRUD


无论用户需求是什么,都是操作系统对内存的部分数据作描述和组织

文件 = 内容+属性,但PCB中的属性与文件中的属性有关系但关系不大,PCB中主要是维护由操作系统创建的一些数据.有关系地方是在需要获取文件属性时需要

 


//linux 进程命令

$ 补充
# pidof 进程名 //显示相关进程的pid (进程栈内所有进程的pid)

用户级工具ps
# ps axj | grep myprocess //查看进程 -- 只查看myprocess进程
# ps axj | head -1 && ps axj | grep myprocess //先管道得到ps axj的第一行(各属性名称), 查看myprocess进程 //管道|优先级比&&高
# ps axj|head -1 && ps axj|grep myprocess |grep -v grep //过滤掉带grep这行 , ps axj|grep后会多一个无关项,过滤掉好看一点

$ 测试执行程序是不是进程 - 执行同一个程序两次 -- 查看是否是同一个可执行程序的两个不同的进程
[chj@expiration1102 1.2进程学习 16:52:10]$ ps axj|head -1 && ps axj|grep myprocess |grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
19970 20647 20647 19970 pts/3 20647 S+ 1002 0:00 ./myprocess
20716 20738 20738 20716 pts/4 20738 S+ 1002 0:00 ./myprocess
//PID是每个进程的编号

# ps aux | head -1
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND

ps
{
# ps -u //只看用户打开的进程
# -e 或 -A:显示所有进程,包括其他用户的进程。
# -l:以长格式显示进程信息,包括更多的详细信息如F字段(进程状态)和NI字段(优先级)。
# -aux:显示所有用户的所有进程的详细信息,通常与grep一起使用以查找特定进程。
# -p:仅显示指定PID的进程信息。
//ps -p PID

# -C:仅显示具有指定命令名的进程。
//ps -C process_name

# -t:显示与指定终端相关的进程。
//ps -t tty_number

# -o:自定义输出格式,可以选择要显示的字段。
//ps -o pid,ppid,cmd


$ ps aux
$ ps ajx

/* 含义 */
1)ps a 显示现行终端机下的所有程序,包括其他用户的程序。
2)ps -A 显示所有程序。
3)ps c 列出程序时,显示每个程序真正的指令名称,而不包含路径,参数或常驻服务的标示。
4)ps -e 此参数的效果和指定"A"参数相同。
5)ps e 列出程序时,显示每个程序所使用的环境变量。
6)ps f 用ASCII字符显示树状结构,表达程序间的相互关系。
7)ps -H 显示树状结构,表示程序间的相互关系。
8)ps -N 显示所有的程序,除了执行ps指令终端机下的程序之外。
9)ps s 采用程序信号的格式显示程序状况。
10)ps S 列出程序时,包括已中断的子程序资料。
11)ps -t <终端机编号>  指定终端机编号,并列出属于该终端机的程序的状况。
12)ps u   以用户为主的格式来显示程序状况。
13)ps x   显示所有程序,不以终端机来区分。
14)ps -l 显示详细PID信息

}


# pstack pid //查看进程的线程栈

 


# ls /proc // 保存进程属性的目录 -- 内存级的文件系统(磁盘上不存在)

/proc内以数字命名的目录是进程信息文件,其名字就是进程的PID

进程文件内其中有两行数据 -- 目前能看懂的,可以证明是执行的程序的进程
lrwxrwxrwx 1 chj chj 0 Sep 14 21:11 cwd -> /home/chj/git_repositories/linux_code/1.2进程学习
lrwxrwxrwx 1 chj chj 0 Sep 14 21:11 exe -> /home/chj/git_repositories/linux_code/1.2进程学习/myprocess

如果中断程序运行,则该进程文件会直接删除
ls: cannot open directory .: No such process //正在访问的目录也会删除
-bash: cd: ..: No such file or directory //cd .. 也执行不了

 

 

$$$$ 学习第一个系统调用
pid_t getpid(void) // 获取进程的PID -- 谁调的getpid就返回谁的pid
pid_t就是int
头文件为#include<unistd.h> 和#include<sys/types.h> //包含两个才不会报错
返回值为进程PID

pid_t getppid(void) //获取父进程id
$ 在测试父进程与子进程过程中,发现每一次重新执行程序,子进程PID改变,但父进程PID不会改变. -- 父进程是谁? -- bash
在通过ps axj|head -1 && ps axj|grep 父进程PID |grep -v grep 查看后发现,父进程是bash
1.bash命令行解释器,本质上也是一个进程
2.命令行启动的所有程序,最终都会变成进程,而该进程对应的父进程都是bash -- (对应上之前学习的bash,bash执行的命令都交给子进程去执行)


$ 杀进程命令
# kill -9 PID // 子进程,父进程,bash都能杀
//杀掉bash后,所有命令都无法正常执行了,所以为了bash安全,bash通过创建子进程方式执行命令
--- linux中没有母进程概念

$$$$ 如何创建子进程 ?
{
系统创建进程有windows打开可执行程序,linux:./可执行程序

$ 创建子进程函数
pid_t fork() //叉子
头文件 #include<unistd.h>
返回值:
On success, the PID of the child process is returned in the parent, and 0 is returned in the child.
On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
成功后,在父进程中返回子进程的PID,在子进程中返回0,父进程返回>0的数。失败时,在父进程中返回-1,不创建子进程,并适当设置errno。
效果:父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

fork在返回时,父子进程都有了,分别在父子进程中ruturn,父进程中return>0,子进程中return 0,总共return两次.返回时发生写实拷贝 -- 看进程地址空间

程序:
printf("AAAAAAAAAAAAAAAAAAAAAAA; pid:%d; ppid:%d; \n",getpid(),getppid());
pid_t ret = fork();
printf("BBBBBBBBBBBBBBBBBBBBBBB; pid:%d; ppid:%d; ret:%d; &ret:%p; \n",getpid(),getppid(),ret,&ret);
结果:
AAAAAAAAAAAAAAAAAAAAAAA; pid:23641; ppid:23234;
BBBBBBBBBBBBBBBBBBBBBBB; pid:23641; ppid:23234; ret:23642; &ret:0x7ffc3c8461fc; //创建子进程成功,返回值是子进程PID
BBBBBBBBBBBBBBBBBBBBBBB; pid:23642; ppid:1; ret:0; &ret:0x7ffc3c8461fc; //子进程的返回值是0
发现:有两个值地址相同的不同值 -- 说明该地址不是物理地址(真实地址),而是间接地址 -- 写时拷贝的体现


$ 执行流 -- 多执行流 -- 宏观现象和单执行流很多差异,如if和else同时存在

$ fork作用
a.fork之后,执行流会变成两个执行流
b.fork之后,谁先运行由调度器决定
c.fork之后,fork之后的代码共享,通常我们通过if和elseif来进行执行流分流
d.fork之后,return需要为两个执行流执行返回代码,即产生两个返回值

$ fork原理:
1.-- 拷贝大部分父进程的PCB,其中指向和父进程同样的内存块 . 小部分属于子进程私有,如子进程自己的PID
2.-- 代码共享 -- 代码只读,不可修改,不影响
3.-- 写时拷贝,当有一个执行流尝试修改数据时,OS会自动给当前进程触发写时拷贝
4.-- 进程具有独立性:1.销毁子进程不影响父进程;2.销毁没有父子关系的进程不会影响别的进程

 

}

 

 

 


$ PCB可以维护在不同的队列中 -- 调度

$ 代码是线性执行的 -- > 我们申请的资源是线性申请的/串行申请 -- 多进程也一样,各自申请各自的

进程状态:
$ 阻塞态: 进程因为等待 某种条件就绪 ,而导致的一种不推进的状态 ,即不被调度,宏观上如卡住了 : 阻塞一定是在等待某种资源,在OS管理的下排队等待
阻塞:进程等待某种资源就绪的过程
为什么阻塞/为什么进入阻塞态? 进程要通过等待的方式,等具体资源被其他对象用完后,再被自己使用
如挂到网卡资源等待队列,键盘等待队列 ... 等各种资源等待队列

$ 挂起态:
内存资源紧张时,会把某些阻塞进程交换到磁盘中(释放内存,腾出空间) -- 挂起状态 ,全称阻塞挂起状态 -- 特殊的阻塞态

//运行态:在运行队列中,可以随时被调度

$ linux中新建状态就是运行态R,没有新建状态,就绪状态也是R -- 操作系统学科中有新建状态,就绪状态,但linux没有
//操作系统学科中是所有具体操作系统的集合,和具体操作系统不同.不同的具体操作系统有不同特点,不一定都有所体现
//操作系统学科又称计算机中的哲学

$$$$ linux进程状态数组task_state_array
The task state array is a strange "bitmap" of reasons to sleep. Thus "running" is zero,
and you can test for combinations of others with simple bit tests.
任务状态数组是一个奇怪的睡眠原因“位图”。因此,“运行”为零,您可以通过简单的位测试来测试其他组合。

$ 进程状态数组 -- task_struct中有int status用于表明该进程的状态
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */ //linux特有的休眠状态
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

一般来说,看一个进程是什么状态,可以看状态,也看这个进程在哪里排队.大部分进程不是运行就是阻塞态

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。运行队列struct task_struct *runquque;
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z(zombie)-僵尸进程:
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,则子进程会进入Z状态
僵尸进程是无法被杀死的 kill-9也不行,因为谁也没法杀死一个死去的东西(进程)
// defunct

僵尸进程危害
.进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
.维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
.那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
.内存泄漏?是的!
.如何避免? -- learn

S:休眠状态 -- 阻塞状态的一种,不在CPU运行,而是在等待某种资源.但确实是在运行队列.由于CPU很快,外设很慢,所以往往在宏观上表现为S,演示:死循环printf可以发现是S
在linux中被称为"可中断休眠",演示:scanf()在等待时就是暂停状态,也可以通过结束程序达到中断.即S状态可以被终止,也可以暂停
例如:拷贝文件到磁盘,则进程进入S,直到拷贝完成

D:不可中断休眠,普通开发一般很难遇到,而做系统管理,运维,IO,存储的经常会遇到
//ps:linux会杀进程
为了保护某些重要的进程不被杀死,所以有了D状态,目的是为了保护某些重要的进程不被杀死.
D状态下的进程无法被杀死,即便是操作系统也不能,甚至无法正常关机,只能断电源(数据损失).只有当D状态的进程恢复时才能杀死该进程
//一般情况下,D状态很难看到(99%),D进程一般都很快执行完毕.但如果D状态有且持续,则可能D进程所在的设备已经快不行了.原因有磁盘快满了,压力过大.再多几个D可能就直接挂掉,宕机了

T:暂停状态
$$ 命令学习: kill -l // 显示kill相关的命令
// 18) SIGCONT 19) SIGSTOP
# kill -19或-SIGSTOP PID //暂停进程 Terminated Stopped //STAT:S+ --> T
# kill -18或-SIGCONT PID //继续进程 ,但会将进程转移到后台 //STAT:T --> S //没有+号了,说明进入后台运行
# kill -9 或-SIGKILL PID //杀死进程 Killed
//SIG : signal 信号 ,TERM terminate 终止
# killall 进程名 //杀掉相关进程的所有进程

※ # jobs -l //显示后台的任务和PID
// 运行中(Running), 已停止(Stopped), 已终止(Terminated)

$ 进程状态STAT带+说明是在前台运行,不带+说明是后台运行
在前台运行则可以通过前台终端命令控制,后台则一些终端命令如前台命令ctrl+C等无效,只能使用shell:kill -9 PID

T 和 S的区别:T往往是操作系统为了阻止某些不合法的任务或用户主动暂停.S是等待资源的阻塞状态
而t(tracing stop) 追踪式暂停:用户主动暂停,如gdb调试断点
$ 运行到断点暂停本质就是进程暂停
验证: gdb打断点,ps axj可以发现STAT为t

X(dead): 死亡状态 -- 瞬时状态,难以观察

$ linux中没有新建态,就绪态等.只有R,运行态,一新建就是R状态

 


$ main函数的返回值是进程退出码 -- 在操作系统/网络/测试用例等方面有作用

 

# echo $? // 查看当前进程结束后的进程退出码
//bash命令也是进程/程序,运行一个命令后也可以使用echo $?查看进程退出码

$ 如果一个进程推出了,立马X状态,立马退出,作为父进程或OS,有没有机会拿到退出结果呢? --拿不到
所以:linux进程退出时,一般进程不会立即彻底退出,而是要维持一个Z状态,也叫做僵尸状态,--方便后续父进程或OS读取该子进程退出的退出结果 -- 可以甄别退出的原因是什么
//进程具有独立性,子进程退出时无法把数据返回给父进程

演示: fork一个子进程,杀死子进程后提示defunct:失效的,死了的
[chj@expiration1102 2status 22:49:08]$ ps axj|head -1&&ps axj|grep out|grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2595 3736 3736 2595 pts/2 3736 S+ 1002 0:00 ./out
3736 3737 3736 2595 pts/2 3736 Z+ 1002 0:00 [out] <defunct>

//僵尸进程如果不回收,会一直占用资源.操作系统也要维护僵尸进程的PCB -->内存泄露

※shell命令
# while :; do ps axj|head -1 && ps ajx|grep a.out|grep -v grep; echo '-------------------'; sleep 1;done

//正常写的程序作为父进程时,我们是看不到僵尸状态的,因为程序一结束就会被bash回收 -- 程序也是bash的子进程 -- 我们写的不对的fork子进程才能看到僵尸进程 -- 内存泄露

$ 1号进程是操作系统, init进程(1号进程) //centos6叫init,centos7后叫systemd(守护进程)
如果子进程没结束,而父进程先结束了,则子进程的父进程会变为操作系统(1号进程)
-- 子进程会被OS自动领养(通过让一号进程成为新的父进程) -- 被领养的进程,叫做:孤儿进程 (Orphan Process)--- 认领和领养机制
-- 如果不领养会发生什么? 后续子进程结束后,无人回收,-->内存泄露 ---- 这种情况只要1号进程init还在就不会发生
-- 所以没有父进程后则自动被OS领养,用于回收子进程
如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
所以孤儿进程不会占资源,僵尸进程会占用资源危害系统。我们应当避免僵尸进程的出现。

 

 


$$$$环境变量environment variables:由系统开机之后,帮我们维护的一些系统运行时的一些动态参数

环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数,
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

常见环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。
USER : 当前用户

// echo PATH //打印PATH
// echo $PATH //读取PATH的内容 // /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/chj/.local/bin:/home/chj/bin
-- '$'符号可以读取文件内容,类似C语言的指针*

$ 环境变量可以存放多个地址,以:作为分隔符,分隔出多个地址,读取顺序从左向右依次寻找

$ 为什么系统命令不用带./也能运行,而我们写的程序却需要./才能运行?
1.首先./是相对路径 --> 是一条明确的位置. 所有程序都需要有路径才能运行
2.执行程序时会先检查环境变量内路径所指的目录,在目录内找到就能执行,找不到就不能执行.显然,指令在其中,路径明确.而我们的程序不在.
所以,我们的程序必须指明路径.当然,命令也可以带路径


$ 在linux中,把可执行程序,拷贝到系统默认路径下,让我们可以直接访问的方式 == 相当于linux下的软件安装
-- rm掉则就是卸载

$ export PATH=$PATH:[newPATH] -- 临时修改 PATH //重新登录后恢复
$ 永久修改所有用户vim /etc/profile
$ 永久修改某个用户vim ~/.bashrc


[chj@expiration1102 ~ 14:47:50]$ env //查看环境变量
XDG_SESSION_ID=170
HOSTNAME=expiration1102 //主机名
TERM=xterm
SHELL=/bin/bash //shell程序
HISTSIZE=1000 //历史命令存储上限
SSH_CLIENT=210.38.241.42 14847 22 //登录的主机
SSH_TTY=/dev/pts/4
USER=chj //当前登录的用户 -- 切换成root后不变

# set //查看更详细的环境变量

# unset 变量 //删除本地变量和环境变量
参数:
-f 仅删除函数。
-v 仅删除变量。//默认


LD_LIBRARY_PATH=:/home/chj/.VimForCpp/vim/bundle/YCM.so/el7.x86_64 //动静态库learn...

//配色方案
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:

MAIL=/var/spool/mail/chj
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/chj/.local/bin:/home/chj/bin //可执行程序的搜索路径
PWD=/home/chj //当前用户所在路径
LANG=en_US.UTF-8 //当前编码
HISTCONTROL=ignoredups
SHLVL=1
HOME=/home/chj //当前所登录用户的家目录
LOGNAME=chj //当前的登录用户 -- 切换成root后不变
SSH_CONNECTION=210.38.241.42 14847 172.19.52.41 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/1002
_=/usr/bin/env


$ main函数最多可以带三个参数
int main(int argc,char* argv[],char* envp[]){return 0};
//char* envp[] //envp是环境变量表, 存放C风格字符串的指针数组 -- 编译器会自动生成并传参进去 //envp:environment variables pointer
//envp的有效元素的下一位存放NULL(表结构).env是environment的缩写
//表结构遍历表不需要额外加判断,到最后一个是null或0自动不满足条件
//char* envp[] == char** envp

$ 指针和指针变量有区别.
指针是一个地址,凡是具有指向能力的数据都可以是指针.
指针变量是一个变量,变量在内存中占有4/8字节,具有数据存储和被修改的能力.保存指针数据的变量叫做指针变量

//1.通过一下代码可以验证envp是一张环境变量表
#include<stdio.h>
int main(int argc,char* argv[],char* envp[])
{
for(int i = 0; envp[i];++i)
{
printf("envp[%d]->%s\n",i,envp[i]);
}
return 0;
}

//2.同样可以通过以下代码获取环境变量表
#include<stdio.h>
#include<unistd.h>
int main()
{
extern char** environ; //unistd内的指针变量:环境变量表,需要声明才可使用
for(int i = 0; environ[i];++i)
{
printf("envp[%d]->%s\n",i,environ[i]);
}
return 0;
}

$ char *getenv(const char *name); //根据环境变量名获取环境变量
#include <stdlib.h>


$ 环境变量本质就是一个内存级的一张表,这张表由用户在登录系统时,操作系统自动给特定的用户形成属于自己的环境变量表
-- 环境变量的内容是从系统的相关配置文件中读取来的
用户环境变量位于~目录下的
-rw-r--r-- 1 chj chj 193 Oct 31 2018 .bash_profile
-rw-r--r-- 1 chj chj 434 Sep 11 22:33 .bashrc

全局环境变量位于/etc下的
-rw-r--r--. 1 root root 2853 Oct 31 2018 bashrc

 

$ 环境变量中的每一个都有自己的用途,有的是进行路径查找,有的是进行身份确认,有的是进行动态库查找,有的是用来进行确认当前路径等等.每个环境变量都有自己的特定的应用场景
每个元素都是kv的 -- <name ,内容>


linux中变量...
[chj@expiration1102 ~ 20:35:28]$ myval=100
[chj@expiration1102 ~ 20:35:35]$ echo $myval //可以创建一个变量
100

这个变量如果是环境变量,则会尾插入在环境变量表.. 其他变量则尾插在其他表(如果存在). 还可能把一个动态开辟的表插进已有的表,再或者另开一个表维护...
内存级就是内存中shell正在维护的表..,数据是由系统的相关配置文件中读取而来.后面加入的变量是直接写在内存,没有写在文件,关闭内存可能就没了
这些表是由shell维护的
例如export可以直接导入到内存级的environmentVariablesTables

$ 环境变量能被所有的子进程继承,验证,export一个变量,然后getenv(),可以证明
环境变量可以被相关的子进程继承下去 -- 环境变量具有全局属性

//main函数是父进程调用...通过一些系统调用..

$ shell的本地变量,只在shell内部有效,不可以给子进程继承,可以通过export 本地变量//导入,就能被继承了

linux shell脚本执行命令时创建子进程问题(特定的情况,例如后台运行管道分支或子shell等,脚本可能会创建子进程执行命令)
Shell脚本在执行每个命令时,不一定会创建子进程。
在大多数情况下,每条命令都在主进程中依次执行,不会创建子进程。
但是,当遇到特定的情况,例如后台运行、管道、分支或子shell等,脚本可能会创建子进程。

//linux下一些奇怪的命令.如'.' , ']' ,

$ 命令行中输入的命令就是一个字符串,以空格为分隔符分隔的就是一个一个的子串
例结构: 可执行程序 -命令选项1 -命令选项2 -命令选项3 ...
0号子串 1号 2号 3号 ...

int main(int argc,char* argv[]){return 0};
argv[0] 就是 0号子串
argv[1] 就是 1号子串
argv[2] 就是 2号子串
...
argv是表结构,也是以NULL结尾.接收的就是命令行中的各个子串构成的表 -- 叫做命令行参数 -- 一般在linux才用得到,windows很少使用命令行
//main会自动分割(strtok)命令,空格或多个空格都是分隔符

argc是表元素个数,有多少个子串,argc就为多少


command line parameter
argc: argument counter
argv: argument value

 


//优先级

优先级和权限:能or不能
优先级:已经能,但是谁先谁后的问题

为什么会有优先级? 资源太少,资源不足 -- 计算机中CPU资源有限

[chj@expiration1102 linux_code 22:45:04]$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1002 10249 10248 0 80 0 - 28920 do_wai pts/1 00:00:00 bash
0 T 1002 12326 10249 0 80 0 - 38008 do_sig pts/1 00:00:00 AppRun
0 R 1002 13810 10249 0 80 0 - 38336 - pts/1 00:00:00 ps

[chj@expiration1102 ~ 10:22:25]$ ps -al
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1002 14684 14607 0 80 0 - 38328 - pts/1 00:00:00 ps


//PRI:priority 优先级:即进程的优先级,就是程序被CPU执行的先后顺序,PRI越小进程的优先级越高.进程的PRI基本都是80,
//NI:nice nice值:代表当前进程所对应的优先级的修正数据,nice值取值范围是-20至19,[-20,19],一共40个级别.一般nice值都是0
NICE:在LINUX系统中,Nice值的范围从-20到+19(不同系统的值范围是不一样的),正值表示低优先级,负值表示高优先级,值为零则表示不会调整该进程的优先级。具有最高优先级的程序,其nice值最低,所以在LINUX系统中,值-20使得一项任务变得非常重要;与之相反,如果任务的nice为+19,则表示它是一个高尚的、无私的任务,允许所有其他任务比自己享有宝贵的CPU时间的更大使用份额,这也就是nice的名称的来意。
https://www.jianshu.com/p/3c078505fffa
//UID(User Identify):user-ID,和用户名的标识,和用户名一对一对应,操作系统用UID标识用户

PRI(new) = 80 + nice ,NI[-20,19], PRI[60,99] //每次调整都是80开始

$ 虽然可以手动调整PRI,但是调度器...对PRI调整的尺度不会太大,尽可能公平. 我们基本也不会去调整优先级 -- 基本全是默认

调整有很多方法,有nice/renice命令,有setpriority(),有top
top方法:进入top,按r(renice),输入PID,输入nice值,回车即可修改

进程具有
1.竞争性:争夺有限的CPU资源
2.独立性:资源独享,多进程运行期间互不干扰,不受其他进程影响
3.并行:真正同时进行
4.并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内让多个进程得以推进的现象 -- CPU疯狂调度,一个进程执行一段时间就被剥离CPU,让下一个进程使用.CPU很快,快到感觉不出来

 

 

进程的独立性:
进程 = 内核数据结构+代码和数据

//父子进程即使是同一份代码,加载进来的数据也会共享,而是各自维护一份.
但是,观察使用同一份代码的父子进程的信息,发现对同一个变量的变量地址是相同的,但父子进程的变量值可以相同 -- 说明我们在语言层面用的地址不是物理地址
--- > 这个地址叫做线性地址或虚拟地址

类型是在应用层实现的,本质是偏移量,通过类型+数据的首地址可以确定一个数据

进程地址空间 -- 就是操作系统画的一张大饼--虚拟内存
//虚拟地址 == 线性地址

操作系统
管理
进程PCB ------------------------------- 进程地址空间 ------------------------------------- 内存

进程地址空间本质就是内核数据结构struct mm_struct{} //memory manage struct

每个进程都有自己的地址空间,这意味着每个进程都有自己的内存地址范围,不会与其他进程冲突。

进程地址空间中的地址是虚拟地址,我们代码平常访问的地址就是虚拟地址
进程地址空间内区域划分就是对各种数据/对象结构体定义后对其赋值约束 -- 数据区,栈区,进程块大小...等等
---- 对线性区域进行指定star和end即完成区域的划分
进程地址空间会根据当前系统自动确定范围,32位则默认4GB,

进程地址空间通常被划分为几个部分:
代码段:存储程序代码的内存区域。
数据段:存储程序运行时所使用的数据的内存区域。
堆:动态分配内存的区域。
栈:存储函数调用时所需的数据(如参数、返回地址和临时变量)的区域。
struct mm_struct
{
long code_start;
long code_end;
long init_start;
long init_end;
...
long brk_start; //堆
long brk_end;
long stack_start;
long stack_end;
}

各种区域的限定区域内的地址就是虚拟地址/线性地址

修改各种区域的大小就是修改进程地址空间内的边界值 -- 简单理解

每个进程在执行时,都会使用自己的地址空间。进程间通信时,必须通过操作系统提供的机制来实现,因为不同进程之间的地址空间是独立的。

进程地址空间32位机器为4GB,64位机器为64GB或1TB:
//每个进程的进程地址空间都是这么大,但不代表进程享有这么多空间.进程使用空间也是需要申请的.会有物理内存不够时申请内存失败

代码中我们一般对变量取地址取的是首地址(低地址),而如何判断一个变量所占的空间和地址规划,则是由类型决定(软件层,本质是偏移量)

我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
程序的地址空间’是不准确的,准确的应该说成 进程地址空间

一个进程:
PCB ------------- 进程地址空间 ------------- 页表 ------------------ 物理内存
(用户级页表)

父子进程 -- 子进程会建立一份基本和父进程一样的数据 -- 未发生写实拷贝时:
父:PCB ------- 进程地址空间 ----- 父页表 --- 物理内存 --- 子页表 ----- 进程地址空间 ------ 子PCB
虚1|实1 实1|虚1

如果子进程有数据发生修改,则发生写时拷贝:(不作修改时指向和父进程一样的物理地址,写的时候再拷贝一份到新空间另作修改,不会影响原数据) -- 体现进程独立性
父:PCB ------- 进程地址空间 ----- 父页表 --- 物理内存 --- 子页表 ----- 进程地址空间 ------ 子PCB
虚1|实1 实2|虚1

如果是父进程先发生修改,那则是父进程发生写时拷贝???? 不确定

fork在返回时,父子进程都有了,分别在父子进程中ruturn,父进程中return>0,子进程中return 0,总共return两次.谁先返回就是谁先发生写实拷贝


进程地址空间和物理内存之间通过页表和MMU(memory manage unit)进行映射和转化
//查找页表的过程是由MMU硬件完成的,不是软件.转换过程是将页表数据导入到MMU中,由MMU进行寻找 --- 软硬件结合
//页表中有权限的字段
//MMU集成在CPU中

页表是KV结构,K存虚拟地址,V存物理地址

 

 

 

OS 进程地址空间之 内核空间(Kernel space) 与用户空间(User space)
[[[[[[[[[[[[[[[[[[[[[[[[[[[[
页表是KV结构,K存虚拟地址,V存物理地址
// 页表还分为用户级页表,内核级页表
$ 内核级页表负责将OS当中的代码和数据,全部都要和物理内存中的区域进行映射,方便我们通过内核空间找到OS的代码和数据 // 可能还有别的数据


$ 功能和区别
1.我们平常代码映射的就是用户空间,栈,堆,共享区(nmap区,未初始化数据区,初始化数据区,正文代码等
2.内核空间存放了OS的代码和数据
3.用户级页表映射的是用户空间,内核级页表映射的是内核空间
4.每个进程的进程地址空间中,用户空间不一样(独立的),但内核空间都是一样的

$ 进程与内核级页表和用户级页表的关系
1. 因为每个进程的用户空间是不一样的,所以每一个进程都有自己的用户级页表
2. 每一个进程的内核空间都是一样的,所以每个进程都能看到同一张内核级页表,所以所有进程都可以通过一个统一的窗口,看到同一个OS!

$ OS运行的本质:OS其实也是在进程地址空间中运行的,OS的运行和 看到OS的内容,与进程切换无关 --- 内核空间怎么切都不变,变的只是不同进程的用户空间
所以,系统调用,实际上,如同调用.so中的方法一样,在自己的地址空间中,进行函数跳转并返回即可.


$ 用户态和内核态
用户态:执行你写的代码的时候,进程所处的状态
内核态:执行OS的代码的时候,进程所处的状态
1.进程时间片到了,需要切换,就需要执行进程切换逻辑(系统代码),由执行操作系统的调度器代码将进程调度出去 --- 发生状态转换
2.系统调用(我们使用的库函数内也封装了很多系统调用,我们经常使用系统调用)


$ 内核态和用户态的作用:
//用户态和内核态是两种执行级别
在执行普通代码的时候,进程是处于用户态,而处于用户态时,OS不允许进程访问内核空间.
当处于内核态时,OS才允许访问内核空间

$ 如何识别内核态还是用户态 --- 软硬件结合的方案
CPU内有CR3寄存器,值为3时表征正在运行的进程执行界别是用户态,0表征正在运行的进程执行级别是内核态.如果不符合,访问内核态,会硬件异常,终止...

$ 用户要想访问内核,必须要有"人"来修改执行界别 ,用户无法直接修改 ---> 操作系统提供的所有系统调用,内部在正式执行调用逻辑之前,会去修改系统级别!
在没有陷入内核时,通过一些中断号来陷入内核.在这里有一段固定的代码,用于帮我们修改执行级别.
----> 所以用户只能通过系统调用来访问系统代码和数据
OS运行是完全依赖于进程的,OS启动的时候,必须有一个进程才能正常运行,(在老式机器中,有个1号进程,就是操作系统),在没有其他进程时,这个1号进程会保持运行
进程是多叉树一样运行的,顶层的就是OS,1号进程 -- systemd

$ OS是如何调度?

a.OS本质是一个软件,是一个死循环.
b.OS内关联着时钟(硬件),主板内有纽扣电池,内部时钟计数器关机后也在一直运行,断网关机一段时间后再开机时钟也不会错
-时钟硬件,每隔很短的时间内向OS发送时钟中断(硬件,向CPU针脚...,OS要执行对应的中断处理方法 --- 这样就会每隔一段时间定期地执行完OS的所有任务
-- 而时钟中断的处理方法是:检测当前进程的时间片,如果时间片到了,则OS就让当前进程调用schedule函数(改变PCB指向)
c.所谓进程被调度,就是时间片到了,然后将进程对应的上下文等进行保存并切换,选择合适的进程 --- 这一整套实际是一个系统函数schedule()完成的
总之,操作系统的进行,是在进程的上下文中运行的 -- 执行系统调用的时候,实际是狸猫换太子,借了当前进程的执行流跑了OS的代码(此时身份已经是OS而不是进程)
--- 系统调用我们只是拿结果,过程是OS在执行


]]]]]]]]]]]]]]]]]]]]]]]]]]]]

 

 

 

 

 

 


虚拟地址空间是操作系统内部为进程创建出来的一种具体的数据结构对象,让进程有统一的视角去看待对应的物理内存,可以让进程管理和内存管理独立开来


虚拟地址空间是发展的产物 -- 初期是直接映射的
这样会存在安全问题,如果我们写的代码不正确,则有可能会直接修改到其他数据,-->会破环系统

$ 虚拟地址空间的作用
1.防止物理地址被随意访问,保护物理内存与其他进程
如果没有虚拟地址,那么会很不安全.有了之后呢,会带一层软件层去保护,识别,判断 -- 有读写检查,越界检查(区域),权限检查...
如不允许修改字符串常量,因为字符串常量的页表权限只读.
还有代码也是只读的--- 对应页表的权限全是只读

2.解耦合 进程管理和内存管理通过页表来沟通,
a.malloc向内存申请空间时,操作系统不会立即提供
b.操作系统一般不允许有任何的浪费或不高效
c.申请内存不一定立马使用 -- 浪费 -- 在申请成功后和使用前这一段时间不使用的话,那么这块空间是处于闲置状态的...
所以:操作系统只会在地址空间中申请一i快内存(此时物理地址处为空),等到真正使用时再分配真实内存空间(提供物理内存) -- 缺页中断 机制

3.可以让进程以统一的视角,看待自己的代码和数据
我们编写的代码在编译成可执行程序的时候,会按区域规则分好各个数据段了.在执行可执行程序,加载到内存时,会按一定规则将各数据段依次加载到内存.
即源代码在被编译的时候,就是按照虚拟地址空间的方式进行对代码和数据早就已经编号好了对应的编制 -- 如linux的ELF格式(百度查阅内部结构)
https://baike.baidu.com/item/ELF/7120560?fr=ge_ala

虚拟地址空间不仅会影响操作系统,还会让编译器遵守这样的规则

写时拷贝保证了父子进程的独立性
写时拷贝是一种按需申请资源的策略 -- 与操作系统不允许浪费资源设定相符
一般只有数据可以发生写时拷贝,多进程时代码也可能发生写时拷贝

fork在网络/服务器中常用
fork失败原因有
1.内存不足
2.系统不允许用户创建太多进程
3.异常退出,如ctrl+c


# echo $? //取得最近一次进程的退出码,只能取一次,取完就刷新,下次取不到了

进程退出情况分类
a.正常退出(1.结果正确 2.结果不正确) -- 提供进程退出码.供用户进行进程退出健康状态的判定 -- 可用可不用
b.奔溃了(进程异常)[操作系统信号 - 奔溃的原因: 进程因为某些原因,导致进程收到了来自操作系统的信号,如kill -9 ]

//打印c语言错误码表 char* strerror(int errnum); //errnum:错误编号 errno。 //no == NO. == number
#include<stdio.h>
#include<string.h>
int main()
{
for(int i = 0; i<200 ; ++i) //不同平台码表不一样 -- 一般不超过255个
{
printf("%d : %s\n",i,strerror(i));
}
return 0;
}

也可以自己定义退出码
const char* err_string[]=
{
"success",
"error",
...
}

进程退出方式
1.return -- main函数中return才是进程退出 --- 进程执行其实是main执行流执行
2.exit函数退出
void exit(int status): <stdlib.h> -- exit可以在代码的任何地方结束进程
参数: status是进程退出码,等价于main的return,0正常退出,非0异常退出

$ exit内封装了_exit(系统调用) -- void _exit(int status):<unistd.h> --- _exit很直接,从系统内直接关闭进程,不会刷新缓冲区什么的 -- main

exit和_exit区别:
1.exit会先执行用户定义的清理函数,然后再冲刷缓冲,关闭流等,在向内核(kernel)申请关闭进程(_exit)
2._exit是直接向内核申请关闭进程
即exit的最后一步是调用_exit

缓冲区不在操作系统内 -- 在用户层中(C/C++库)
如果在系统内的话_exit也会刷新缓冲区


进程退出方式:信号+退出码方案 // 信号(异常)或退出码(正常退出结果对不对)

 

什么是进程等待?
进程等待就是通过系统调用,获取 子进程退出码或退出信号 的方式,顺便释放内存问题 -- 获取子进程信息,然后释放子进程

进程等待的目的
1.避免内存泄露
2.获取子进程执行的结果(如果必要) -- 根据进程退出方式

如何进程等待?
系统调用wait/waitpid

wait : wait for process to change state
声明: pid_t wait(int *status);
头文件 <sys/types.h> <sys/wait.h>
功能:父进程一直在等待子进程,子进程不退出,父进程也不会退出 -- 子进程一挂就能回收了
返回值:
wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.
wait():成功时,返回被终止子进程的进程ID;出现错误时,返回-1。


waitpid:/
pid_t waitpid(pid_t pid, int *status, int options);

参数:
1.pid
如果pid>0,(是进程号) 则waitpid会一直等待该进程
如果pid == -1 , 则等待任何一个进程,等价于wait

2.status 状态码
int* status:输出型参数
32位位图结构 --
0000 0000 0000 0000 | 0000 0000 0000 0000
第16-31位不使用
第8-15位:退出码(exit code):退出码:return或exit的status //打印退出信息的编号
第0-6位:退出信号(exit signal),终止信号:0为正常退出,即没有收到信号(只有信号为0时才看退出码,此时退出码为0的进程才是正确执行).如果退出码不为0,虽然正常执行,但是结果不对
如果退出码不为0,则
第7位(core dump标志) ...
作用:是否发生core dump ,0表示不发生,1表示发生
//正常退出状态core dump为0,异常时可能为1
验证:
{
核心转储关闭时,core dump是否会为1? 开启时?
1. 关闭时core dump flag不受影响,始终为0;
2.开启时,异常会置1
即:core dump开启时,如果发生异常,core dump flag会置1,并且生成core.pid文件

正常终止:
[15 退出状态 8][未使用][6 未使用 0]
被信号所杀:
[15 未使用 8][ Core ][6 终止信号 0]
┖-------------------------------- Core dump flag//核心转储标志位

3.options
a.WNOHANG:W(wait) NO(没有) HANG(夯住了:一般机器卡顿时,网络堵塞时我们称为夯住了)
让进程立刻返回,不等待进程
b.0:一直等待 -- 阻塞等待

在C语言中,你可以使用一些宏来处理进程退出码,其中最常见的宏是WIFEXITED,它用于检查进程是否正常终止,并且可以获取进程的退出码。
1.WIFEXITED(status):如果进程正常终止,WIFEXITED(status)将返回一个非零值,否则返回0。
"W" 表示 "wait","IF" 表示 "if","EXITED" 表示 "exited"//表示 "如果进程已经退出"。
2.WEXITSTATUS(status): 这个宏用于获取正常终止的进程的退出码。如果WIFEXITED(status)返回非零值,你可以使用WEXITSTATUS(status)来获取进程的退出码。
"W" 表示 "wait","EXITSTATUS" 表示 "exit status"。 表示 "等待并获取退出状态"。

3.WIFSIGNALED(status) - "W" 表示 "wait","IF" 表示 "if","SIGNALED" 表示 "signaled"。
因此,WIFSIGNALED(status) 表示 "如果进程因信号终止"。这个宏用于检查进程是否因为接收到信号而终止。
4.WTERMSIG(status) - "W" 表示 "wait","TERMSIG" 表示 "termination signal"。
因此,WTERMSIG(status) 表示 "等待并获取终止信号"。这个宏用于获取导致进程终止的信号的编号。如果WIFSIGNALED(status)返回非零值,你可以使用WTERMSIG(status)来获取信号的编号。


返回值: -- 返回等待成功的子进程,>0 .失败返回-1 -- 一般只有pid出错了才会失败
waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist,
but have not yet changed state, then 0 is returned. On error, -1 is returned.
成功时,返回状态已更改的子进程的进程ID;如果指定了WNOHANG(非阻塞轮询)并且存在由pid指定的一个或多个子进程(ren),
但尚未改变状态,则返回0。出现错误时,返回-1。


pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。


学习使用信号1~31号信号 -- 这些信号都是宏 #define SIGHUP 1 //信号也是一个数字
[chj@expiration1102 ~ 18:34:02]$ 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

没有0号信号
SIGFPE 8 为除零
SIGKILL 9 为主动杀死
11) SIGSEGV 为空指针异常/野指针

wait和waitpid如何拿到进程数据? -- 父进程是如何拿到子进程的退出信息的
1.进程PCB内部有两个属性:int exit_code和int exit_signal
其中main函数会把进程退出码写到exit_code中,操作系统会把信号/异常信息写到exit_signal中
2.通过系统调用wait和waitpid访问内核中的task_struct(PCB)取得数据并设置到输出型参数status中 -- 系统调用有权限访问内核

父进程在wait的时候,如果子进程没有退出,父进程在干什么? --- 只能一直调用wait/waitpid等待 -- 阻塞等待 -- 直到子进程完成后再唤醒父进程
子进程PCB内有task_struct *parent; // 通过它可以找到父进程
这过程父进程状态: R->S->R

非阻塞轮询:即wait不会一直等待进程,而是多次检查 -- waitpid(pid,status,WNOHANG)

 


$ 创建子进程的目的是什么? --- 让子进程帮我执行特定的任务
1.让子进程执行父进程的一部分代码 -- 但代码的归属权还是属于子进程
2.如果想要子进程指向一个全新的程序代码呢? -- 进程的程序替换 /为什么要有进程的程序替换

子进程
功能1:在原来的代码的基础上进行
功能2:执行不同的,全新的代码


$$$$$ shell 里的进程替换(Process Substitution)

$ exec系列函数
SYNOPSIS/synopsis :概要
#include <unistd.h>

extern char **environ; //系统环境变量表指针

//加载器
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

* int execve(const char *filename, char *const argv[], char *const envp[]);//系统调用 -- 其他6个都是封装了execve
//没有execlpe

//使用注意,命令行参数必须要以NULL结尾


$ 命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索自己的环境变量PATH
e(env) : 表示自己维护环境变量 -- 不带e的就是使用系统环境变量

//执行程序流程:1.找到该程序(路径) 2.如何执行该程序(命令选项)

$ 只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。

 

exec的返回值:
RETURN VALUE
The exec() functions return only if an error has occurred. The return value is -1, and errno is set to indicate the error.
exec()函数仅在发生错误时返回。返回值为-1,并设置errno以指示错误

exec替换成功后不会有返回值,虽然声明带了返回值int,也没必要看了,因为查看返回值的代码都被替换掉了,有也看不到 -- 应该是有的,只是看不到了

exec失败:返回-1,并会继续执行后面的代码

子进程exec成功后,父进程可以通过退出码去检查exec后的代码执行的如何了


$ 接口解析
# exec要么带v,要么带l
-- 只要有arg就一定要传 "程序名" , ... ,NULL ,数组也一样,结尾要带NULL
# 不带e的接口是会默认继承父进程的环境变量表的,即exec(...) 等价 exece(...,environ);

1.execl
参数path:是程序路径.绝对路径或相对路径都可以. arg是命令行参数,和main的命令行参数一样,按命令拆分成字符串的格式一样,一个一个传进去,最后以NULL结尾
2.execv
参数path和execl一样,只是arg换成了argv,即一个一个传的参数换成了传字符串数组,数组元素就是execl的arg序列,最后以NULL结束
3.execlp
参数arg和execl一样,一个一个传命令子串. 参数file是程序名称,而程序路径由系统自动从环境变量中查找
4.execvp
5.execle
参数envp是自定义环境变量 -- 一般用于在子进程不想使用父进程的环境变量的时候 --
自定义环境变量表 -- 自定义环境变量会覆盖式(清空原来的环境变量)写入
# putenv 将自己的环境变量加到系统的环境变量中
头文件:#include<stdlib.h>
DESCRIPTION
The putenv() function adds or changes the value of environment variables.The argument string is of the form name=value.
If name does not already exist in the environment, then string is added to the environment.
If name does exist, then the value of name in the environment is changed to value.
The string pointed to by string becomes part of the environment, so alteringthe string changes the environment.
说明
函数的作用是添加或更改环境变量的值。参数字符串的格式为name=value。
如果名称在环境中不存在,则字符串将添加到环境中。
如果name确实存在,则环境中name的值将更改为value。
字符串指向的字符串成为环境的一部分,因此更改字符串会更改环境。

## 操作系统能通过带e的exec把环境变量传给子进程 -- 子进程就获得了自己的环境变量

任何一个子进程都是fork后再exece执行的 验证: bash中export一个变量,fork并execle得到孙子进程,验证孙子进程的环境变量已继承bash的环境变量

$ exec是系统调用 -- 只要是进程都能调用.所有程序最后都是进程,所有任何语言都可以


$ 创建进程时,先创建PCB再把数据从磁盘加载到内存 -->创建子进程时,先创建PCB,再exec加载进内存...
即父进程先调用exec,再去调用了其他程序的main...

$ 程序替换只会影响当前调用的进程:谁调了exec就替换谁 --- 进程的独立性 --> 子进程则会发生写时拷贝(代码区),不影响父进程
--- 在单进程时代码区一般不可修改 ,多进程时为了进程的独立性需要对代码区作修改


C++源文件可以使用“.c++”,“.cc”,“.cxx”作为后缀名

$ 程序替换:老进程使用了全新的程序(没有创建新的进程) - 原来的代码被用新的代码替换掉了 --- 内存代码段中旧代码被新代码覆盖了,不再执行旧代码
只在必要时就该页表的映射关系,而内核数据结构并不做任何的修改

 

$$$$ 简易shell

不是所有命令都让子进程去执行,需要自己做的命令被称为内建命令


内建/内置命令:让bash自己执行的命令 -- 如cd这样的,不应该让子进程跑的,如果给子进程执行,则是子进程改了目录,而bash没改目录.没有意义
{
int chdir(const char *path); -- 系统调用:用于改变调用该接口的进程的工作路径
头文件:#include <unistd.h>
参数path:要更改的路径

# type 命令 // 说明某条命令是内建命令还是外部程序

 


$ 一般用户自定义的环境变量,在bash中要用户自己来维护,不要用一个经常被覆盖的缓冲区来保存环境变量
如argv,如果环境变量保存在argv,因为argv经常要用来存放解析后的命令子串,维护不当有可能会覆盖掉环境变量,导致环境变量失效
--- 解决方法可以将argv解析出的环境变量转移到其他变量保存
//几乎所有的环境变量命令都是内建命令
//只要维护好了bash自己的环境变量,子进程会自动继承父进程的环境变量

}

 

 

 

 

 


$$$$$$$文件

$ linux中,文件被打开,OS会对被打开的文件进行管理,即要创建对应的内核数据结构 --- 和进程类似

$ 文件可以被分成两大类 1.磁盘文件(文件系统) 2.被打开的文件(文件操作)

//文件可以同时被多个进程打开

# du -b/k/m 文件 //显示文件大小
# du -h 文件/目录/路径/通配符 //自动单位
//linux以4kb为单位的,空文件内存为0,不足4k的文件显示为4kb


$ c语言文件模式/权限
{
r Open text file for reading. The stream is positioned at the beginning of the file.

r+ Open for reading and writing. The stream is positioned at the beginning of the file.
// r+和w+都是读写方式打开,区别是r+不创建文件

w Truncate file to zero length or create text file for writing. The stream is positioned at the beginning of the file.

w+ Open for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.

a Open for appending (writing at end of file). The file is created if it does not exist. The stream is posi? tioned at the end of the file.

a+ Open for reading and appending (writing at end of file). The file is created if it does not exist. The initial file position for reading is at the beginning of the file, but output is always appended to the end of the file.
}


linux文件系统调用


打开文件open
声明:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

RETURN VALUE(fd:file descriptor)
open() and creat() return the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).

int open(const char *pathname, int flags, mode_t mode);
功能:打开或创建文件
pathname:文件路径
flag:打开文件时的权限
O_RDONLY 只读 ReaD only
​ O_WRONLY 只写 WRite only
O_RDWR 读写 ReaD WRite
​ O_CREAT 文件不存在时则创建 create
​ // O_EXCL 配合创建使用,文件存在时出错 //待学
​ O_TRUNC 清空 truncate //默认不清空
​ O_APPEND 追加 append //追加和清空逻辑互斥,正常不会同时出现
mode:创建文件时的权限 mmm 与chmod的权限设置方式一致
返回值:
文件描述符
​ 失败返回-1
————————————————
https://blog.csdn.net/weixin_51420051/article/details/110937524


// close目前仅掌握使用方法....
close应该就是把文件描述符表的某个下标的元素设为nullptr,即不指向任何文件

OS接口传递标记位的方式 -- 位图
{
int flags; --> int有32个比特位,每一个比特位对应一个标记位,一个int可以同时传32个比特位,足够使用

}


linux关闭文件接口close
声明:
int close(int fd);
头文件:
#include <unistd.h>
返回值:
功能:close应该就是把文件描述符表的某个下标的元素设为nullptr,即不指向任何文件

自定义掩码umask
声明:
mode_t umask(mode_t mask);
头文件:
#include <sys/types.h>
#include <sys/stat.h>
RETURN VALUE
This system call always succeeds and the previous value of the mask is returned.
返回值:此系统调用始终成功,并返回掩码的上一个值。

使用:用户设置的umask优先于系统默认的umask --- 就近原则
umask(0000) // 注意:0开头是八进制数字


$ write //linux写入接口
声明:
ssize_t write(int fd, const void *buf, size_t count);
头文件:
#include <unistd.h>
返回值:
返回实际写入字节数,0为没有写入任何字节,失败返回-1
描述:
count是一次最多写入的字节数


$ snprintf //把缓冲区写入到字符串接口
声明:
int sprintf(char *str, const char *format, ...);
Return value
Upon successful return, these functions return the number of characters printed (excluding the null byte used to end output to strings).
返回值
成功返回后,这些函数将返回打印的字符数(不包括用于结束字符串输出的空字节)。


$ read() //linux从文件读入接口
声明:
ssize_t read(int fd, void *buf, size_t count);
头文件:
#include<unistd.h>
返回值:
返回实际读入的字节数
描述:
count是一次最多读取的字节数,返回值是读到的字节数

 

 

$$$$ 文件描述符fd -- file_descriptor

生命周期:随进程结束而关闭

$ 任何一个进程在启动时,默认会打开当前进程的三个文件:
标准输入, 标准输出, 标准错误 ---- 本质都是文件
C语言: stdin stdout stderr ---- 文件在语言层的表现 FILE *stdin;FILE *stdout;FILE *stderr
C++ : cin cout cerr ---- 同上,不过是一个类
键盘文件 显示器文件 显示器文件 ---- 设备文件
fd码 : 0 1 2 ---- 文件描述符descriptor

//通过a.out > log.txt 发现:标准错误不会进入文件,而是直接到显示器
$ 标准输出和标准错误都会向显示器打印,但其实不一样

$ 由于进程的三个文件会自动打开,所以不需要我们手动打开,可以直接调用输入输出

$ 我们打开的文件的文件描述符fd为什么最小是3? 因为进程启动时默任启动了3个标准文件,分别是0,1,2. 所以fd只能是3开始.
而后续打开的文件的fd可以是依次递增的:3,4,5,6,7,8... ------------------数组下标(open的返回值)
也可以不是递增的,如果中途关闭某个文件,那这个文件的位置就会被回收,下一个新打开的文件就是使用该下表
-- 进程中,文件描述符的分配规则:在文件描述符中,最小的,没有被使用的数组元素,分配给新文件
// 验证:如果把0或1或2号关闭了再分配,会发现不是std而是新文件了


$$ 进程与文件

$ 内存中文件描述与组织
1.文件也和进程一样,OS会对每一个打开的文件进行管理,在内存中构建struct file结点,该结点存放了文件的元数据,文件的操作方法及缓冲区等,并组织成结点为struct file的数据结构.
2.一个进程可以打开多个文件,每一个进程与文件都是是1:n的关系,所有文件的struct都在OS所管理的文件内核数据结构中.

$ 进程管理自己打开的文件的方式
3.OS为进程定义了一个struct file_struct结构体结点. 能够维护进程与由该进程打开的文件的映射关系,快速为进程找到自己打开的文件,
4.该结构体内定义了一个数组,类型为struct file *fd_array[],内存放了进程自己打开的文件的struct file.通过遍历数组就能找到对应的文件.
默认前3个存放了标准文件,即下标0,1,2分别存放标准输入,标准输出,标准错误stdin,stdout,stderr
5.进程控制块task_struct中定义了struct files_struct *files指针变量,该指针变量files指向了struct files_struct结构体 === 进程 - files_struct - fd_array - 文件
7.对新打开的文件OS会遍历数组,找到空位后插入,并返回下表 --- 哈希表,映射表
8.要找到对应的文件,只需要通过对应下标元素就能得到文件结构体的地址

10.实现了进程管理与文件系统的解耦合,只通过指针和文件描述符耦合,联系.没有深度联系 -- 用地址方式轻耦合,用数组下表进行快速索引

9.每个文件都有一个缓冲区,这个缓冲区位于file结点内,而所谓的IO类read,write函数,本质是拷贝函数,--- 用户空间和内核空间进行数据的来回拷贝,
write用于把内容拷贝到文件缓冲区.至于什么时候从文件缓冲区写入到磁盘指定位置(缓冲区刷新)则由操作系统决定 -- 不同操作系统对应不同的刷新策略
read是把内容从磁盘拷贝到文件缓冲区中,等需要时再拷贝到我们指定的缓冲区

11.一个文件只会被打开一次,即只有一个文件结构体,其他进程打开这个文件不会再构建文件结构体
文件结构体内部有对应的引用计数,记录了多少个进程打开该文件


$$$$ 理解linux下一切皆文件
-------------------------------------------------------------------------------------------------------------


我们使用操作系统都是通过进程的方式进行的 -- 如QQ,微信,浏览器等等各种程序都是进程-- 进程的视角就是用户的视角 -- 人类访问计算机都是以进程为载体进行访问的
而进程只能看到文件,所以一切皆文件


task_struct -> (*files) -> files_struct

files_struct --> _____
| ....
进程 -- | ...
| file* -|0| _ <-- stdin
| file* |1| | <-- stdout
| file* |2| - - file* fd_array[] <-- stderr
|---------------------- | ... -|n| <-- 其他文件
| |_____ ------------------|
| |
\|/ \|/
--------------------------------------------------------------------------------------------------------------

struct file struct file
{ {
//文件权限 //文件权限
//文件的大小 //文件的大小
// ... // ...
//当前文件对象自己的缓冲区 //当前文件对象自己的缓冲区
文件对象 --
int (*readp)(int fd,char buffer[],int size); int (*readp)(int fd,char buffer[],int size);
int (*writep)(int fd,char buffer[],int size); int (*writep)(int fd,char buffer[],int size);
//其他函数指针 ... //其他函数指针 ...
} } ...
通过函数指针,再面对底层不一样的驱动程序时只需要调用对应底层的方法就可以了,不需要关心底层实现的差异化
操作系统只需要把上层的数据拷贝到缓冲区里,再调用底层不同设备对应的读写方法就可以把数据放到不同的外设里 ----- 因此从linux上层看,以下一切皆文件
无论底层再怎么差异化,linux通过函数指针的方式屏蔽了他们的差异

--- C语言设计面向对象的方法
-------------------------------------------------------------------------------------------------------------
...(其他接口)
驱动程序 -- read_keyboard(); read_screen();(空) read_netcard();
write_keboard();(空) write_screen(); write_netcard();
-------------------------------------------------------------------------------------------------------------
外设 -- 键盘 显示器 网卡 ....

 

 

---------
$ 操作系统层面,必须要通过fd才能找到文件

$ 任何语言IO一定会使用write和read,而使用WR和RD一定要由fd,所以一定会封装了fd. 如C语言的FILE结构体就封装了fd(_fileno). --> C++的cin,cout等也一定封装了fd

//C语言静态库后缀名一般为.so ,动态库一般为 .a
//C语言动静态库路径 /lib64 或/usr/lib64
//C语言头文件路径 /usr/include

 

作业1:模拟输出,输入,追加重定向
1.分别关闭stdin ,stdout,stderr

$ 输出重定向和追加重定向都是往1号文件输入
close(1);
int fd = open(LOG,O_WRONLY|O_CREAT|O_APPEND,0666); //追加重定向
int fd = open(LOG,O_WRONLY|O_CREAT|O_TRUNC,0666); //输出重定向

作业2:观察1和2号fd,使用a.out > file

$ 标准输出和标准输入不一样.输出的文件不一样,out是1号文件,err是2号文件
//输出重定向只改1号重定向,不改2号重定向

作业3:将输出到stdout的数据重定向到文件1,将输出到stderr的数据重定向到文件2

$ stdout和stderr作用不同,目的是将正常信息和错误信息分开 -- 方便错误调试,如果混在一起,看起来很麻烦


总结:
重定向的完整写法:
输出重定向# command n>file //n是下表,是fd. 1可以省略,默认不带数字就是1. n>1后就不能省略
输入重定向# command<n file //n默认是0(stdin) ,默认省略. n>0后就不能省略

特殊: 2>&1 // 2 and 1 ,2和1一起绑定到1.即2和1一起输出到1

//注意重定向>在命令中的位置,依旧是从左往右,顺序不一样执行的效果不一样

#include <unistd.h>

int dup2(int oldfd, int newfd);//即:把老下标对应的字符/流式文件 指向到 文件描述符表的新下标的位置,fd就是下标

oldfd 是要复制的原文件描述符。
newfd 是要将原文件描述符复制到的目标文件描述符。
dup2() 的基本工作原理如下:

1.如果 newfd 已经打开,则 dup2() 会先关闭 newfd。
2.然后,dup2() 会将 oldfd 复制到 newfd,使得它们指向相同的文件描述符表项。这意味着它们将共享相同的文件状态信息,包括文件偏移和文件状态标志(例如,读写模式)。
3.如果 newfd 和 oldfd 的值相同,dup2() 会直接返回 newfd,并且不会关闭它。


官方原话:
makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:

* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.

使newfd成为oldfd的副本,必要时先关闭newfd,但请注意以下内容:

*如果oldfd不是有效的文件描述符,那么调用将失败,并且newfd不会关闭。
*如果oldfd是一个有效的文件描述符,而newfd的值与oldfd相同,则dup2()不执行任何操作,并返回newfd。

 

$$$$ 缓冲区

缓冲区结构
内存 外存(没有加载进内存的)
语言缓冲区 -- 文件缓冲区(包括外设文件,如显示器) -- 磁盘


$ 三种语言级缓冲策略
1.无缓冲 //来一个刷新一个 --
2.行缓冲 //遇到换行符就刷新 -- 输出的文件是显示器文件时刷新策略是行缓冲
3.全缓冲 //写满缓冲区再刷新 -- 输出的对象是普通文件时刷新策略是全缓冲

为什么要有缓冲区?
因为系统调用需要时间,时间成本较高,使用缓冲区策略能节省调用者时间 --- 减少刷新次数,减少调用系统接口次数

C库的缓冲区在FILE结构体中,每次打开文件都会生成 -- FILE结构很复杂

系统调用write没有缓冲区,直接写给操作系统指定的输出对象(文件)了
-- 至于什么时候从文件缓冲区写到内存,属于操作系统的策略

 

用户强制从系统刷新到磁盘 -- synchronize a file's in-core state with storage device -- 将处于核心状态的文件与存储设备同步
声明 int fsync(int fd);
头文件#include <unistd.h>

printf函数输出的实际是一个一个的字符,并不是千百十量级的打印 -- 就像人写字一样,也是一个一个字的写
--- printf实际是 格式控制 功能的函数,将不同类型的数据转换成一个一个字符

 


$$$$ 磁盘文件

打开的文件
1.管理文件 -- 先描述再组织 -- struct file
2.进程的关联问题 -- 文件描述符
3.文件的读写问题 -- 语言级别和内核级别的缓冲区
4.刷新问题 -- 刷新策略

没打开的文件
5.文件如何合理存储问题 --
合理存储能够 a.快速定位 b.快速读取和写入
--
如何标识文件:a.文件名

...

 

 

 

 

 

 

--------------------------------------------------------------------------------------------------
磁盘的物理结构
1.了解磁盘结构
磁盘(机械硬盘...)是我们计算机上唯一的机械设备 -- 磁盘与固态硬盘物理结构不一样,磁盘是机械结构,ssd是电子结构

扇区(sector): 将磁道划分为若干个小的区段, 就是扇区. 虽然很小, 但实际是一个扇子的形状, 故称为扇区. 每个扇区的一般大小为512字节.


磁盘结构: https://www.cnblogs.com/lsgxeva/p/15641934.html
柱面(磁道)磁头扇区CHS定位法

C:cylinder 柱面
H:head 磁头
S:sector 扇区

 

2.逻辑抽象
OS不能直接和使用硬件,要和磁盘做好解耦工作 -- 因为OS是软件,而硬件会更新换代,一旦硬件发生变化,没有解耦的操作系统则将不能正常使用...
a.设磁盘扇区为512字节(固定),这是很小的基本IO数据量.而OS实际进行IO是4KB(可以调整)
--- 基本单位是OS与外设交互时的一次读写大小,无论需要的数据多小,1B也要读4KB --- 即一块 -- 所以磁盘设备一般称为块设备
--- 文件系统读取时按一块进行进行读取
--- OS按块读取数据能减少IO次数 ,能解耦合...
-- OS需要一套新的地址(独立一套) 来进行块设备的访问

引入:计算机领域中一种耦合与解耦合的解决思想: 在软件和软件或软件和硬件中增加一层新的软件层就可以提出新的解决方案
软件学科有一条公理:在任何的软件中,没有解决方案时,可以增加一层软件层来增加解决方案.

$$ 定位扇区的方式:LBA 定位扇区方式:
{
磁道 - 扇区 展开图(将一条磁道断开成一条直线,然后依次连接后面的磁道)
[0][1][2][3][][][][][][][][][][][][][] ... [0][1][2][3][4][5][][][][][][][][][][][][][]
磁道0:/ 数组下标 -- 逻辑抽象 磁道1;/

逻辑块地址(Logical Block Address, LBA)是描述计算机存储设备上数据所在区块的通用机制,一般用在像硬盘这样的辅助记忆设备。
LBA可以意指某个数据区块的地址或是某个地址所指向的数据区块。现今计算机上所谓一个逻辑区块通常是512或1024位组。

一个LBA地址就对应一块扇区 -- LBA+连续读取8个扇区就得到一个块

}

OS把LBA地址的块当作一种类型,和语言类型一样,首地址+偏移量方式得到下一块 -- 一块=n个扇区

$$ 磁盘与OS互相转换 LBA < -- > CHS
H=heads per cylinder,每个磁柱的磁头数 -- 默认为1,忽略
S=sectors per track,每磁道的扇区数
#c= lba / ( S*H )
#h= ( lba/S ) % H
#s= ( lba%S ) + 1

LBA -> CHS : 设一个盘面有5000个扇区,设一个磁道有1000个扇区.而有其中一个LBA是6500,求CHS
H(先确定在哪一面,即确定哪个磁头): int h = 6500/5000 = 1 ; 所以在第2个磁头,自低向上第二面上.即1号盘面
C(求第几柱面,即第几个磁道): int c = 6500/1000= 6 ; // 6号磁道,第7个磁道 //自底向上的所有盘面的所有磁道都连在一起
S: int s = 6500%1000 = 500 ; //一个磁道的第500号扇区

 

CHS -> LBA:
H=heads per cylinder,每个磁柱的磁头数 -- 默认为1,忽略
S=sectors per track,每磁道的扇区数
lba = ( c*H + h )*S + s - 1
lba = ( c+h )*S + s - 1

总结:OS对磁盘管理转化成了对数组的管理 -- 先描述再组织 -- struct block{};


3.文件系统
文件系统分为两套:磁盘上的和内存中的. 我们编程一般指内存中的.

$$ 分区分组管理

对磁盘分区 struct disk{struct part[5]; //...};
分区大小 struct part{int lba_start; int lba_end; //... }; //part:区域
对区分组 struct part{struct part group[100]; //... }; //两个struct part一样的,取其内容

一个磁盘可以分成几个区,每个区可以分成多个组Block group 0,1,2,...

Boot_block
1.每个区开头会有一点区域Boot_Block用于保存操作系统启动相关的内容.如分区表,操作系统的镜像地址等,一般位于0号盘面的0号磁道的1号扇区开始保存着,在C盘的某个区域.
2.一般开机至少要做两件事情,第一步先找到磁盘设备并加载磁盘的驱动程序,第二步是加载的分区表(识别出C,D,E盘...),
然后再从特定分区的开始位置读取到操作系统对应的镜像的地址,然后找到操作系统在磁盘的位置,然后加载操作系统
3.一般如果因为一些原因而导致了Boot_Block的数据丢失,如数据刮花了,数据没了,那么操作系统就不能启动了
4.除了Boot_Block这个区域是与开机相关外,其他区域都是与数据相关
5.Boot_Block也可能有备份 -- 看文件系统 -- 但规定开机只找0号盘面0号柱面1号扇区

文件 = 内容+属性
linux中文件和内容是分离的 -- linux把文件内容和文件属性分离
文件的内容和属性都要以块的形式,被保存在磁盘的某个位置

在一个分组中,主要字段分为6个
1.超级块 SuperBlock(SB)
2.组描述符表 GroupDscriptorTable(GDT)
3.块位图 BlockBitmap
4.索引节点位图 inodeBitmap
5.索引节点表 inodeTable
6.数据块 Data blocks

$ SuperBlock(价值上很重要):保存了整个分区文件系统的所有属性信息,有
1.文件系统的类型
2.整个分区的情况:如一共分了多少组,每一个的使用率是多少,可用空间,文件系统其他信息等...
//EXT是延伸文件系统(英语:Extended file system,缩写为 ext或 ext1),也译为扩展文件系统,一种文件系统,于1992年4月发表,是为linux核心所做的第一个文件系统。
采用Unix文件系统(UFS)的元数据结构,以克服MINIX文件系统性能不佳的问题。它是在linux上,第一个利用虚拟文件系统实现出的文件系统,在linux核心0.96c版中首次加入支持,最大可支持2GB的文件系统
$ 每一个组都可能存在一个和其他SuperBlock完全相同SuperBlock,且会统一更新 -- SB区域损坏后整个分区不可被使用,因此需要做好备份 -- 多副本保证分区安全策略
//为什么其他块不做多副本? 因为数据价值大!如果是boot_block损坏只是单纯开不了机,而数据还在,可以恢复.而如果SB坏了,那整个分区就无法恢复了.其他字段影响范围有限,可以承担

超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,
未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的
时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个
文件系统结构就被破坏了

$ GDT:类似SB,保存了自己所在组的详细的统计属性信息,还有各字段的分布情况,区域位置...

$ inodeTable:
一般而言,一个文件,内部所有属性的集合,称之为inode/节点,大小一般是128字节 --- 一个文件对应一个inode.
每个区都都有很多文件,所以每个group都需要有专门保存所有文件的inode的区域 --- inode表inodeTable -----------------
inode表可以看作一个数组,每个元素都是128字节
每一个inode都有自己的编号 --- 区分
文件的属性是基本相同的,因此固定大小

$$ inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。
一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。
假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。
查看每个硬盘分区的inode总数和已经使用的数量,可以使用df命令。

$$ 每个inode都有一个号码,操作系统用inode号码来识别不同的文件。 -- inode_number
这里值得重复一遍,Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。
表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:
首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

$$ Unix/Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。
目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码。

$ Data Block
文件的内容是变化的,因此使用数据块来进行文件内容保存,一个文件的内容需要有[1,n]个数据块来保存 --- 即便只有1个字符也要占用1个块
因为有多个文件,因此有了存放文件内容的区域Data Block -- 单位大小4KB,OS中与外设交互的子系统是文件系统

//linux查找1个文件,是根据inode编号来进行文件查找的,包括读取内容
//struct inode{ int blocks[NUM]; //... }; // blocks中存放了inode对应的所有数据块的编号 -- inode和data_block的映射关系

//每个group中基本都是数据块,只有极小部分是inode等其他信息 -- 因此OS启动时会直接把inode等全部加载进内存,而数据块只在需要时在加载进内存

$ inodeBitmap:每个bit表示一个inode是否空闲可用,下标为inode编号,值为1表示在工作,为2表示不在工作

$ blockBitmap:每个bit表示一个DateBlock是否空闲可用

# ls -i // 命令选项-i能查看inode

$ inode VS 文件名
linux系统只认inode号,文件的inode属性中不存在文件名! 文件名是给用户使用的
$ 目录是文件,目录也有inode.目录也有内容,所以目录也有数据块
$ 任何一个文件,一定在目录内部,所以目录的内容是什么呢? 目录的数据块里保存了 该目录下文件名和文件inode编号对应的映射关系.在目录内,文件名和inode互为key值

$ 加载一个文件:
1.现在当前目录下找到该文件的inode编号(通过文件名映射关系).
2.一个目录也是一个文件,也一定隶属于一个分区,结合inode编号,在该分区的inodeTable找到该inode编号对应的inode结构体
3.通过该inode结构体对应的datablock的映射关系,找到该文件的数据块,然后加载到内存

$ 查找一个文件:读取该目录的内容
$ 删除一个文件:查找inode再根据inode...把block bitmap和inode bitmap对应的比特位 置为0即可,不需要清空 -- 只需要修改位图
$ 增加一个文件:从低到高扫描目录所在组的inodebitmap,找到为0的比特位(后置为1)就是新的inode,再把属性填到inodeTable,然后在目录文件的内容中追加入新的文件名和inode映射关系即可.

//linux维护有删除文件的日志,会维持一段时间 -- 数据恢复
$ 回收站就是一个目录,从回收站删除才是修改位图

$ inode在一个分区内有效,不能跨分区,因为通常一个分区是一套文件系统.
$ inode能确定分组,在一个分区内,不同的组享有不同范围的inode,整个区的inode编号是唯一的,区内不同组享有不同区间的inode编号
每个组的inode_bitmap的编号是从0开始的,真正的inode编号是 组起始位置的inode编号+位图中的编号

//格式化:OS向分区写入文件系统的管理属性信息 //linux命令 # mkfs.ext(2/3/4)

$ 多级索引 -- linux采用的索引方式
inode表 --- 数据块
前k1个inode采用直接映射方式对应数据块
k2个inode为二级索引,间接寻址方式,间址,第一次直接指向的不是存放目标文件数据的数据块,而是存放其他数据块的编号,通过这些编号,可以1对多找到多个数据块 -- 扩展了容量 -- 两层/三层的多叉树
k3个inode为三级索引,需要间址三次...
...

$ 存在inode消耗完,还剩datablock的情况 -- 创建大量空文件而不写数据 //”no space left on device”
$ 存在inode剩余,datablock消耗完的情况 -- 创建一个文件,写入大量数据
// 两种情况都不能解决

$$ 文件系统的属性虽然是在磁盘中,但发生修改时,根据冯诺依曼体系,它们只能在内存中被修改,操作完成后再统一刷新回磁盘中


联系与总结: 文件描述符fd -- inode表 -n- inode属性 -1- DataBlok -- 多级索引

4.软硬链接

建立软链接:# ln -s 路径 软链接名 //不加软链接名则软连接名默认是最后一个/的文件/目录名
lrwxrwxrwx softLinkFile -> file // l开头是链接文件

软连接是一个独立的链接文件,有自己的inode 编号,有自己的inode属性和内容
软链接文件内容放的是自己所指向的文件的路径
//删除软连接的目标文件后软连接会闪红
作用:快捷方式
$ 软连接路径是绝对路径则是全局 /是相对路径则不能移动到其他目录
# readlink 软链接 //显示软链接的路径

建立硬链接:# ln 旧名 新名
硬链接和目标文件共用同一个inode编号,硬链接和目标文件使用同一个inode,没有独立的inode
硬链接内建立了 新文件名和老inode的映射关系
//建立硬链接后,直到硬链接数为0才能彻底删除该文件

$ 作用:
1.节省硬盘空间。同样的文件,只需要维护硬连接关系,不需要进行多重的拷贝,这样可以节省硬盘空间。
2.重命名文件。重命名文件并不需要打开该文件,只需改动某个目录项的内容即可。
3.删除文件。删除文件只需将相应的目录项删除,该文件的链接数减1,如果删除目录项后该文件的链接数为零,这时系统才把真正的文件从磁盘上删除。
4.文件更新。如果涉及文件更新,只需要先在WinSxS目录里面下载好一个新版本,然后修改Windows\System32下面同名文件的硬连接关系,从旧版本的硬连接指向新版本的硬连接,这样就能够快速的完成文件的更新工作,而不需要进行文件的复制,速度也会快不少。
5.卸载补丁。遇到需要补丁卸载的情况,只需要把硬连接指向改为旧版本就可以了,没有文件替换的问题。而且建立了硬连接关系的文件之间的修改是同步的,因此只要有一方被修改了,另一方也会得到修改。


//增加硬链接后,硬链接数+1 --- 硬链接数是一种引用计数 -- 引用技术==有多少人指向我
-rw-rw-r-- 2 chj chj 0 Oct 9 15:15 my-hard
lrwxrwxrwx 1 chj chj 12 Oct 9 22:36 my-soft -> new_file.txt
-rw-rw-r-- 2 chj chj 0 Oct 9 15:15 new_file.txt

$ 软连接支持对目录创建,硬链接不支持 作者:竞予科技 https://www.bilibili.com/read/cv20365690/ 出处:bilibili

每个目录内都有有.和..文件, .是当前目录的硬链接, ..是上级目录的硬链接 -- .和..只能由OS维护,用户不能修改(root也不行),保证安全
-- . 和 .. 是OS特殊维护的可成环的路径结构,此结构OS可以识别,如果是用户则可能会成环,造成换路路径问题,破坏路径唯一性
所以:硬链接不支持对目录创建 -- 硬链接数-2就是当前目录内至少具有的文件夹


$ 删除文件的另一种方式
# unlink //

 

----------------------------------------------------------

ACM时间
由于访问是最为频繁的操作,如果频繁的修改访问时间,则会占用很多的资源,所以只访问的话access时间一般会有一段时间才会更改/刷新
change时间和modify时间则 只要有修改则一定会刷新

 

 

 

 

 

 

 

---------------------------------------------------------
1.C/C++的库

// C/C++的生态,一般是开源(直接给代码),要么就是给库(编译好的二进制文件)
不同语言区别一般是解决问题的方式和生态,如python/java一般是给包,或其他能够直接使用的东西

//评价一款语言的好坏主要看其生态和应用场景:
应用场景中如C/C++主要是高并发高可用高扩展,python/java一般是快速搭建,快速使用


C语言的静态库: /usr/lib64/libc.a
C语言的动态库: /usr/lib64/libc-2.17.so

C++的动态库: /usr/lib64/libstdc++.so.6s

//库的真实名称为 去掉前缀lib 去掉后缀.so/.a(如果有版本号也要去掉)
//一般的云服务器默认只会存在动态库,不存在静态库,静态库需要单独安装

c/c++头文件 /usr/include/
c++头文件 /usr/include/c++/

//头文件提供声明
//库提供实现

//编译器语法提示,自动补全功能需要包含头文件 -- 自动将用户输入的内容在头文件中搜索 --- 依赖头文件
//语法检查,报错是编译器在编辑器后台自动编译,但不链接的自动化模式

2.库有什么用

提高开发效率

//学习阶段,造轮子是最好的学习方式
//开发阶段 -- 用轮子

3.库的设计角度 -- 打包给别人用


4.如何使用库


静态库:
{

制作库:
# ar rc lib库名[.so或.a] 要打包的文件1 要打包的文件2 ... //ar是打包命令archive r是选项replace:替换已有的或插入新的; c是不存在则创建;
//ar,Linux系统的一个备份压缩命令,用于创建、修改备存文件(archive),或从备存文件中提取成员文件。ar命令最常见的用法是将目标文件打包为静态链接库。
https://baike.baidu.com/item/ar/7426017 //ar百度百科

//静态库的.o和一般的.o是一样的.只有动态库的.o是特殊的

//使用库除了要有库之外还要有头文件
//我们安装别人的库一般就是下载库和对应的头文件,安装到系统的默认路径下

初步演示:
当前目录中已有文件:add.h libmath.a main.c sub.h
# gcc -o a.out main.c -lmath -L. -I.
//-l(lib-name)后接要使用的库的库名(真实库名); -I(include):后接头文件路径; -L(lib-dir):后接库所在的路径 //选项后可以不带空格 // .是当前目录
//不指明路径则在默认路径下寻找

//由于gcc不加 -static选项 ,实际上只有静态库是静态链接的情况. 其他依赖的第一第二方库都是动态链接的. --- ldd和file可以验证
//加 -static后,所有库都是静态链接

实际使用: ---- 库的安装
1. 把下载下来的库的inlucde内的头文件拷贝到系统的/usr/include/中
2. 把下载下来的库的lib内的库文件拷贝到系统的/usr/lib64/中
//命令需要提权 sudo
3.然后gcc main.c -l库名 //编译成可执行程序 -- 之后就不依赖该静态库了 //此时就不需要-L 和 -I了 //如果不安装则需要
$ 安装和卸载的本质就是 拷贝到系统特定的路径下! ----- 安装/卸载就是拷贝


//使用第三方库(除了第一方和第二方,剩下的都是第三方):即便是全部安装到系统中,也必须要指明使用哪个库. 如gcc/g++要用-l指明具体的库名
第一方:编程语言提供的库...
第二方:操作系统系统接口...

实际使用 ---
无论是库还是源代码,源代码会提供内置的编译方式,如makefile,cmake等. --- 最终都会提供一个make install(安装)的命令(脚本) ---- 实际就是拷贝,安装到系统
,由于要安装到系统,所以大部分指令都需要sudo或超级用户


1.静态库非常占资源,可执行程序体积变大占用内存;下载周期变长,占用网络资源 ;
}


5.动态库的配置
{

# ldd 动态链接的可执行程序 //查看可执行程序依赖的动态库

生成与位置无关码的.o文件:
# gcc -fpic -c 文件 // position ignore code //形成一个 与位置无关码 的.o文件 -- 和静态链接的.o文件有差别
//打包成动态库
# gcc -shared -o libmath.so *.o; ///shared 共享
//编译器自带动态库打包功能说明:使用动态库是主流

如果使用静态库的编译方式://报错: ./a.out: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
问题在于:编译器编译过程正常识别,但执行过程需要经过OS,而OS默认只会在系统的默认路径下查找库
//为什么静态库能找到? 因为静态库已经把库拷贝到可执行文件中了,编译完成后可以不依赖库执行

解决方法1:环境变量 LD_LIBRARY_PATH//指定 load_库_路径 //LIBRARY_PATH是库路径
https://blog.csdn.net/weixin_42617472/article/details/125829895
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH://home/chj/.../lib //临时导入环境变量
//此时查看ldd就能发现可执行程序找到了动态库
注意:仅在本次登录有效,登出后环境变量就重置了

解决方法2:在lib64中创建软连接
# ln -s /home/chj/.../lib/libmath.so /lib64/libmath.so
卸载:删除软连接即可

解决方法3:配置文件
在路径:/etc/ld.so.conf.d/ 下新建配置文件 --- //加载动态库配置文件
1.配置文件名可以自定义
2.配置文件内只存放目标文件路径
3.在可执行程序所在目录底下使用sudo ldconfig命令使配置文件生效
//ldconfig是一个动态链接库管理命令,命令的用途,主要是在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索出可共享的动态链接库(格式如前介绍,lib*.so*),
进而创建出动态装入程序(ld.so)所需的连接和缓存文件.缓存文件默认为 /etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表.

//etc 是 "etcetera" 的缩写,表示"其他"或"等等"的意思。这个目录的名称源自拉丁语,最初用于存储各种系统配置文件以及其他不属于特定目录的文件。在Linux系统中,/etc 目录包含了各种配置文件,而名称 "etcetera" 暗示了这是一个包含各种杂项配置文件的地方。
/etc 文件夹在Linux系统中具有重要的作用,它通常包含系统级别的配置文件和设置。以下是 /etc 文件夹的一些常见用途:
1.系统配置文件: /etc 文件夹包含了许多系统级别的配置文件,用于配置操作系统和安装的软件。这些文件包括网络配置、用户帐户信息、系统服务启动脚本、硬件设备配置、安全策略等。例如,/etc/network 包含网络配置文件,/etc/passwd 包含用户帐户信息。
2.软件配置文件: 许多安装的软件包也会在 /etc 中创建配置文件,以供系统管理员进行配置。这些文件可以用于自定义软件的行为,包括数据库、Web服务器、应用程序和服务。
3.启动脚本: /etc 文件夹通常包含用于配置系统启动和关机过程的脚本文件,如 /etc/init.d 或 /etc/systemd 目录。这些脚本用于启动和停止系统服务,以及执行其他初始化任务。
4.日志和日志配置: /etc 文件夹中的一些子目录包含了日志文件以及日志记录工具的配置文件。这些文件对于系统监控和故障排除非常重要。
5.安全策略和权限设置: /etc 文件夹中包含了一些与系统和应用程序的安全策略有关的文件。例如,/etc/security 目录包含了安全策略的配置文件,/etc/sudoers 文件包含了sudo权限设置。
6.默认配置文件: /etc 文件夹通常包含了系统和应用程序的默认配置文件,当安装新软件或服务时,这些文件会作为模板使用。
总之,/etc 文件夹在Linux系统中起着关键的作用,它存储了许多配置信息,允许管理员对系统的行为进行调整和自定义。这使得系统在不同环境中可以进行灵活的配置。

//解决方法4:拷贝到系统默认路径..

}

优点是编译后不依赖相关的库文件

6.动态库的加载
可执行程序执行后加载到内存成为进程,在执行过程中遇到需要的库函数的地址时,检测内存中该库是否被加载,没有加载则把库加载进内存,然后通过页表映射到进程PCB中
不同进程可以共用一份动态库,只需要通过各自的页表映射到各自的PCB中即可 -- 最终映射到共享区(nmap区)中
$ 加载库不一定是全部加载进内存,库可能会很大,可以需要哪部分再加载


$ 磁盘上的可执行程序内有自己的逻辑地址,加载到内存中有物理地址,PCB中有虚拟地址
静态链接的可执行程序的地址按0到FFF... 进行编址 --- 绝对编址 , 然后再通过页表映射

//地址就两类绝对编址和相对编址

每个进程的共享空间中空闲位置是不确定的
动态库中的所有地址,都是相对于整个库的起始地址的偏移量,默认从0开始.动态库只有加载进内存时,起始地址才能被确定 -- 因为动态库是需要时再加载,不需要时不能占有空间,要给其他对象使用
而静态库已经在确定在可执行程序中的位置了,固定不变

//先搞明白静态库的地址,就能明白动态库了

静态库(Static Library):
静态库是一组编译好的目标文件的集合,它们在链接时被直接嵌入到可执行文件中。这意味着在编译期间,编译器将静态库的代码和数据部分合并到可执行文件中,
形成一个单一的可执行文件。因此,静态库中的代码和数据在编译时就被决定了其在可执行文件中的位置,使用绝对地址。

动态库(Dynamic Library):
动态库是一个独立的文件,它在运行时被加载到内存中,而不是在编译时被合并到可执行文件中。这使得多个可执行文件可以共享相同的动态库,
节省了磁盘空间并允许在系统上动态升级库。因为动态库在加载时才确定其位置,所以它们不能使用绝对地址。

 

$$$ 实验
1.静动态库同时存在时,默认采用动态库
2.如果只有静态库,则静态链接第三方库,动态链接第一第二方库

sudo yum install -y ncurses-devel
// ncurses库
https://blog.csdn.net/zty857016148/article/details/132124383

 


////////////////////////////////////////////////////////////////////////////////////////////////////////////////
VSCODE

rpm包主要用于redhat及分支如redhat,centos,Fedora等
deb包主要用于debian及分支如debian,ubuntu等。


~/.vscode-server/extensions/ 目录下是vscode的插件位置
vscode与linux异常 --> 删除.vscode-server,下次使用vscode链接linux会自动重建


C++中包含C头文件:
{
在C++中,标准C库的头文件通常存在两个版本,一种是不带.h扩展的,另一种是带.h扩展的。例如,有两种版本的头文件分别是 <stdio.h> 和 <cstdio>。

不带.h扩展的头文件(例如 <cstdio>):
这些头文件是C++标准库的一部分,并包含了C标准库的函数和变量。它们将C标准库的函数和变量放在std命名空间中,因此在使用时需要前缀std::。例如,<cstdio> 中的 printf 在C++中应该使用 std::printf。

带.h扩展的头文件(例如 <stdio.h>):
这些头文件是C标准库的一部分,它们不在std命名空间中。在C++中,您可以包含这些头文件,但它们不会将函数和变量放在std命名空间中,因此可以直接使用,不需要前缀std::。

在新的C++代码中,通常建议使用不带.h扩展的头文件,将C库的函数和变量置于std命名空间中,以避免全局命名冲突并更好地与C++的标准库协同工作。
然而,对于现有的C代码,或者在需要与C库进行交互时,您可能需要包含带.h扩展的头文件。在新的C++项目中,推荐使用C++标准库的相应头文件,如 <iostream> 代替 <stdio.h> 和 <cstdio>,以获得更好的类型安全性和C++特性。

}


////////////////////////////////////////////////////////////////////////////////////////////////////////////////

 

 

 

 

 

 

 

 

 

 


进程间通信 IPC Inter-Process Communication //实际上是构建进程间通信的解决方案

$$ 进程间通信分类:
1.管道
匿名管道pipe //Anonymous pipes
命名管道
2.System V IPC // V仅表示名称 或者version意思
System V 消息队列
System V 共享内存
※ System V 信号量
3.POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁


前言:
$ 进程是具有独立性的,进程间通信不能破环进程的独立性
$ 要让两个不同的进程进行通信,前提是:先让两个进程看到同一份资源-----OS直接或间接提供
$ 一定是一个进程创建一个进程获取

$$
任何进程通信手段都需要经过一下两步
a.想办法,让不同的进程看到同一份资源 -- 看到同一份资源的方式不同,资源不同->通信方式的差异
b.让一方写入,一方读取,完成通信过程.其他通信目的与后续工作,要结合具体场景

$ 管理IPC:
1.谁创建,谁负责 //服务端负责提供资源和回收资源


IPC和文件系统类似,但却是不一样的两套独立机制,而linux一切皆文件,所以IPC会边缘化...

 

1.管道
{
$ 什么是管道
管道是Unix中最古老的进程间通信的形式。 -- 所有IPC的爷爷
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道

管道自带互斥属性

# who | wc -l //最基本的进程间通信

验证管道|两边的进程是否是独立进程:
[chj@expiration1102 ~ 18:45:42]$ sleep 10000 | sleep 20000 |sleep 30000 & //&的作用是让进程在后台进行
[chj@expiration1102 ~ 18:51:12]$ ps ajx |head -1 && ps axj |grep -E '16473|sleep' | grep -v grep //-E 正则表达式
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
16472 16473 16473 16473 pts/1 16844 Ss 1002 0:00 -bash
16473 16522 16522 16473 pts/1 16844 S 1002 0:00 sleep 10000 //不一样的三个进程在同时进行
16473 16523 16522 16473 pts/1 16844 S 1002 0:00 sleep 20000
16473 16524 16522 16473 pts/1 16844 S 1002 0:00 sleep 30000 //
15508 16817 15508 15508 ? -1 S 1002 0:00 sleep 180
16473 16844 16844 16473 pts/1 16844 R+ 1002 0:00 ps axj


$ linux中一切皆文件 -- 管道也一样是文件

//管道的大小是有上限的,不同的平台不一样, linux的上限是65535 ,2^16

管道是一个内存级文件(由操作系统提供的,不需要刷新到磁盘中的文件) --- 匿名文件 //该文件主要是提供给两进程通信用的,临时就够了,不需要写到磁盘

左进程将标准输出重定向到管道,右进程将标准输入重定向到管道

管道实现原理: -- 匿名管道方式
1.进程以r和w方式分别打开同一个文件(如文件描述符3和4)
2.然后fork
-- fork创建子进程只会复制进程相关数据结构对象,而不会复制内存这边的文件结构,即复制后两个进程依旧指向同一份资源 --- 满足了两个进程看到同一份资源的前提
此时一个进程往该文件中写入,另一个进程从该文件中读入 -- 即可实现进程间通信 -- 但此时这种方式只支持单向通信
//原理:复制进程/子进程继承了父进程打开的文件描述符
3.确定数据流向,关闭不需要的fd:
-- 子进程关闭r的文件如3,父进程关闭w的文件如4,此时可以反向通信


// 为什么管道只支持单向通信? 因为文件一次只能读或写,所以才被命名成管道
// 如何同时双向通信? 定义两个管道即可
// 为什么这种方式叫匿名管道? 因为这个文件是匿名文件,用户不知道这个文件的名字和位置


#include <unistd.h>
功能:创建一匿名管道
原型
int pipe(int fd[2]);
参数
输出型参数fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端 -- 返回管道的读端和写端的文件描述符
返回值:成功返回0,失败返回错误代码
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

int ppidfd[2];
ppidfd[0] //读端 -- 0:嘴巴:读东西
ppidfd[1] //写端 -- 1:笔 :写东西

$ 管道单向通信原理:
{
1.创建管道
|------写端------|
进程 管道
|------读端------|

2.进程复制
|------写端------| |---写端---------|
进程 管道 子进程(进程复制)
|------读端------| |---读端---------|

3.关闭不需要文件描述符
|------写端------|
进程 管道 子进程(进程复制) //单向通信
|---读端---------|

}

管道的特点:
1.单向通信 --- //半双工(Half Duplex)数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输
//全双工:通信允许数据在两个方向上同时传输
2.管道的本质是文件,因为fd的声明周期随进程,管道的声明周期也随进程
3.使用管道的进程具有继承关系,如父子进程. 爷孙进程,继承父亲的兄弟进程也可以 -- 通常用来给 具有'血缘'关系的进程 进行进程间通信
4.管道中写入次数和读入次数不是严格匹配的 -- 管道中数据是字节流形式 -- 都是一个一个的字符
(读写次数的多少没有强相关 --- 字节流表现)
5.管道大小有上限

//管道读写是互斥,独立的 ..写的数据不会影响读的数据,读的数据不会影响写的数据

$$ 管道读写演示实验

a.写一次,读一次
现象:每次读到的数据都是下一次写入的数据,不会读到已经读取过的数据
// 读过的数据相当于被删除了(其实是覆盖)

b.快写入,慢读取:
现象:管道写满后就不能再继续写,-->管道有上限

c.慢写入,快读取:
现象:当管道没有数据时,读进程会一直等待(阻塞)写进程写入,直到写进程写入数据

d.写端关闭,读端一直读 :
现象:一直读

e 读端关闭,写端一直写: //
现象: OS主动杀死写端进程 --> OS不会维护无意义,浪费资源的资源 --- 13号信号sigpipe

$ 关闭进程池的必要操作
1.要让使用管道的子进程退出,只需要让父进程关闭所有的write_fd就可以.
严格来说应该是关闭一个子进程的全部写端,才能把这个子进程关闭
2.父进程回收子进程的僵尸状态


$$ 进程池:
{
//预先开辟好的管道程序称为进程池

一.直接构建法
1.建第一个管道,然后进程复制
|------写端------| |---写端---------|
进程 0号管道 0号子进程(进程复制)
|------读端------| |---读端---------|

2.关闭父进程读端,关闭0号子进程写端
|------写端------|
进程 0号管道 0号子进程(进程复制)
|---读端---------|

3.创建第二个管道
|------写端------|
进程 0号管道 0号子进程(进程复制)
| | |---读端---------|
| |
| |------写端------|
| 1号管道
|---|-----读端------|

4.进程复制,复制出第二个子进程,1号
|------写端------| |------写端-------------|
进程 0号管道 |
| | |
| | |
| |------写端------| |------写端------| |
| 1号管道 1号子进程
|---|-----读端------| |------读端------|


5.实际情况:
|------写端------|
进程 | 管道 0号子进程/复制进程
| | | |---读端---------|
| | |
| | |
| | |--------------------------------|
|
| | |
| |-|------写端------| |---写端---------| |
| 管道 1号子进程/复制进程
|---|------读端------| |---读端---------|


现象:从第二进程开始,每复制一个进程都会复制之前存在的所有管道的写端

$ 注意事项:关闭进程池的其中一个进程时,必须先把所有的写端全部关闭
解决方法1: 反向关闭子进程,最后一个进程一定只有1个(来自父进程)写端,关闭后次低位的进程的非父进程的写端也全部关闭了,只剩父进程的...

解决方法2:构建一个单纯一对一的标准进程池


}

 


file结构体中 file文件是个union类型,分为普通文件和管道文件 -- 因此操作系统能够区分开


# man 7 pipe //查看管道的详细信息
PIPE_BUF是一个宏,宏一般大小是4096 = 2^12
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
------>> 进程在对管道进行读写时,会有冲突的可能.如果是原子操作,则可以保护冲突,让读写互斥. 否则,将不保证读写正确
//原子性现象:要写100个数据,如果没有写完,则一定读不了,要读只能等写完100个. --- 原子性:多执行流交替执行访问共享资源时,安全访问的一种保护手段

//管道提供的是流式服务 -- 体现:字节流
//管道的声明周期:随进程
//一般而言,内核会对管道操作进行同步与互斥

如果一端关闭写,则OS会杀死另一端(发送13号SIGPIPE命令),因为OS不会允许资源浪费(阻塞) --- 实现一个进程控制另一个进程


.hpp文件允许类的定义和声明不分离,成员方法的声明和实现不分离..通常开源项目用.hpp来做,使用文件较少,


# 回顾:
$ 进程间通信的目的是
1.数据传输:一个进程给另一个进程发送数据
2.资源共享:多个进程之间共享同样的资源
3.通知事件:发送消息...
4.进程控制:发送信号...

}

 


$$$$ 命名管道 Named Pipes
{
前言: 管道(匿名管道)的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信

描述:命名管道可以在不相关的进程之间交换数据,他是一个FIFO文件,是一个特殊类型的文件,是一个可以显式创建出来的文件,是一个内存级文件(不需要刷盘的文件)
// 有inode等,但没有datablock(不需要刷盘)

如何让不同的进程看到同一份命名管道? // 文件的唯一性:路径. 通过路径+文件名让两个不同的进程看到同一个命名管道
//命名管道为什么叫命名管道? 就是因为该文件是有文件名的,而且必须有文件名


命令:
mkfifo --- make FIFOs (named pipes) //创建一个FIFO(队列)文件

# mkfifo fifo //创建一个名为fifo的命名管道
prw-rw-r-- 1 chj chj 0 Oct 22 13:29 fifo //以p开头,说明是管道文件

演示1.: 两个终端
1号终端输入 echo "hello world" > fifo //之后1号终端会进入阻塞状态
2号终端输入 cat < fifo //读取到hello world ,然后1号终端解除阻塞状态

演示2:
1号终端输入 while :; do echo "hello";sleep 1; done > fifo
2号终端输入 cat < fifo

两边都是独立的进程,没有亲缘关系

现象: 命名管道有一端未打开,进程会阻塞在open处 -- open的特性,可以关闭,带上某命令选项即可

$$ 必须先启动读端,读端控制进程是否正常,
1.读端开启,会阻塞/非阻塞等待写端.
2.读端关闭,则写端进程关闭.

}

 

system V (版本的) 共享内存 //shared memory共享内存
{

system V是一套标准,专门为为了通信而设计出来的一个内核模块

$ 共享内存也其他IPC一样,必须要让两个进程看到同一份资源
$ 共享内存的生命周期不随进程,随OS
$ 共享内存是所有IPC中最快的.
$ 共享内存如数组,虚拟地址是连续的
$ 访问共享内存可以不使用任何接口. 一旦共享内存映射到进程的地址空间,该共享内存就能被所有的进程看到了
$ 共享内存没有保护机制(同步互斥)

拓展: 使用管道和共享内存组合.1.让一个管道从共享内存中读数据,另一个管道发信号 2.一个管道直接发控制信号,然后直接访问共享内存....

//所有进程都能看到的资源叫公共资源

$ 共享内存的使用流程;
1.创建共享内存
2.关联进程和取消关联
3.释放共享内存

$ OS中有很多共享内存 ---> OS需要管理共享内存 --- 存在管理shm结构体的数据结构
//共享内存结构体struct shm{}; //存放了共享内存的所有属性
共享内存 = 共享内存结构体+为共享内存开辟的空间


linux 共享内存的接口
{
一.shmget//创建共享内存
声明:int shmget(key_t key, size_t size, int shmflg);
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
参数:
1.key: 类似inode,用于唯一标识块,可以任意值,一般使用ftok来设置key.
OS会遍历共享内存的key,创建共享的内存的进程会检查,如果key不存在,则设置成新共享内存的key.获取共享内存的进程使用同样的key去配对共享内存
2.size:申请的共享内存块的大小
size equal to the value of size rounded up to a multiple of PAGE_SIZE. (大小等于四舍五入到PAGE_size倍数的大小值)
共享内存的大小是以PAGE页(一般大小为4kb)为单位的,分配空间最小为1块(0,4kb,8kb,12kb ...)
3.shmflg: //宏,和open的宏类似 一般使用IPC_CREAT|IPC_EXCL的组合,保存创建的是独享的共享内存
IPC_CREAT:创建共享内存.如果共享内存不存在,则创建之. 如果共享内存已存在,则获取已存在的共享内存的并返回
IPC_EXCL:Exclusive:表示如果共享内存段已经存在,则创建失败并返回,确保该内存段是独占的,不与其他进程共享。如果共享内存段已经存在,并且使用IPC_EXCL标志创建新的共享内存段,则会返回一个错误。
这有助于防止多个进程创建相同的共享内存段,从而确保共享内存的独占性。
--- 不能单独使用,一般配合PC_CREAT
返回值:
1.错误返回-1
2.成功返回有效的共享内存标识符,由于和文件描述符冲突(网络都是使用文件描述符),所以不是很常用,

二.ftok //将路径名和项目标识符转换为System V版本的key
$ 确保提供一个已存在的文件路径,因为ftok会根据文件的inode号和给定的整数生成唯一的key。
//ftok是一套算法,没有涉及太多系统接口
声明:key_t ftok(const char *pathname, int proj_id);
参数:
1.pathname:文件路径
2.proj_id:项目id
返回值:
1.错误返回-1,并设计errno...
2.正确返回key值

三.shmctl //SystemV版本的shm控制
shmctl 是Linux/Unix系统中的一个系统调用,用于对共享内存段进行控制操作。它通常用于管理共享内存,包括创建、删除、获取信息和控制共享内存的属性等操作。
声明int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
1.shmid:是共享内存段的标识符,通常是由 shmget 返回的标识符。
2.cmd:是要执行的操作,可以是以下值之一:
IPC_STAT:获取共享内存段的信息,将其存储在 struct shmid_ds 结构体中,该结构体通过 buf 参数传递。
IPC_SET:设置共享内存段的属性,将 struct shmid_ds 结构体中的信息应用到共享内存段。
IPC_RMID:从系统中删除共享内存段。
其他特定于系统的命令,具体取决于系统和使用情况。
3.buf:输出型参数.一个指向 struct shmid_ds 结构体的指针,用于存储或设置共享内存段的信息。通常,buf 参数在执行 IPC_STAT 和 IPC_SET 操作时被使用。
返回值:
当执行 IPC_STAT 命令时,shmctl 返回 0 表示成功,并将共享内存段的信息存储在提供的 struct shmid_ds 结构体中。
The caller must have read permission on the shared memory segment. //要有读权限
struct shmid_ds { //data structure
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
当执行 IPC_SET 命令时,shmctl 返回 0 表示成功,表示成功更新了共享内存段的属性。
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
当执行 IPC_RMID 命令时,shmctl 返回 0 表示成功,表示成功删除了共享内存段。
如果出现错误,shmctl 返回 -1,并设置 errno 变量来指示发生的错误。

四.shmat //System V 版本 shm 操作 -- 类似malloc在虚拟地址上申请空间,
类似malloc,返回一段虚拟内存
声明:void *shmat(int shmid, const void *shmaddr, int shmflg); //attaches
参数:
shmid:要关联的shmid
shmaddr:选择挂接的内存地址,用户不知道,所以设为null,让系统选择即可.
shmflg:SHM_RDONLY(只读),0(读写)
返回值:返回虚拟地址(数组),供用户使用

五.shmdt //detaches(拆卸)
类似free,不需要大小,可以通过维护数组的结构体知道大小
声明:int shmdt(const void *shmaddr);



}

}


三对命令:
ipcs //ipc
{
ipcs 是 "Inter-Process Communication (IPC) Status" 的缩写

ipcs 是一个Linux命令,用于查看进程间通信(Inter-Process Communication,IPC)的相关信息。它通常用于查看系统中的消息队列、信号量和共享内存等 IPC 对象的状态。以下是一些常用的 ipcs 命令选项和相关单词的解释:

Message Queues(消息队列):
ipcs -q:查看消息队列的信息。//queue
msqid:消息队列的标识符。
key:消息队列的键值。
mode:访问权限和标志。
cbytes:消息队列的当前字节数。
qnum:消息队列中的消息数量。

Semaphores(信号量):
ipcs -s:查看信号量的信息。 //singal
semid:信号量的标识符。
key:信号量的键值。
mode:访问权限和标志。
nsems:信号量集中的信号量数量。
otime:上次操作时间。
ctime:创建时间。
semnum:信号量的编号。

Shared Memory Segments(共享内存段):
ipcs -m:查看共享内存段的信息。//memory
shmid:共享内存段的标识符。
key:共享内存段的键值。
mode:访问权限和标志。
owner:拥有者的用户ID。
cpid:创建该共享内存段的进程ID。
lpid:上次连接到该共享内存段的进程ID。
IPC Control Commands(IPC 控制命令):

ipcrm:用于删除 IPC 对象(消息队列、信号量、共享内存)。
-q, -s, -m:用于指定要删除的对象类型。
-Q, -S, -M:用于指定对象的标识符
# ipcrm -m/s/q shmid/msqid/semid //删除消息队列.信号量,共享内存

$ ipc操作方法类似文件,fd==id ,key==inode. 在用户层也和文件一样使用,使用fd/id. key和inode给操作系统使用

--------------------------------------------------------------------------------
centos下ipcs

------ Message Queues --------
key msqid owner perms used-bytes messages

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status

------ Semaphore Arrays --------
key semid owner perms nsems

key(Hex) id(Dec)

Message Queues (消息队列):
key:消息队列的键值,用于标识消息队列。
msqid:消息队列的标识符,系统为每个消息队列分配一个唯一的标识符。
owner:消息队列的拥有者,即创建该消息队列的用户。
perms:消息队列的访问权限和权限位。
used-bytes:消息队列当前使用的字节数。
messages:消息队列中的消息数量。

Shared Memory Segments (共享内存段):
key:共享内存段的键值,用于标识共享内存段。
shmid:共享内存段的标识符,系统为每个共享内存段分配一个唯一的标识符。
owner:共享内存段的拥有者,即创建该共享内存段的用户。
perms:共享内存段的访问权限和权限位。 //permission
bytes:共享内存段的字节数。
nattch:连接到共享内存段的进程数量。 //attached:所附的,附加的 -- 关联数
status:共享内存段的状态信息。

Semaphore Arrays (信号量数组):
key:信号量数组的键值,用于标识信号量数组。
semid:信号量数组的标识符,系统为每个信号量数组分配一个唯一的标识符。
owner:信号量数组的拥有者,即创建该信号量数组的用户。
perms:信号量数组的访问权限和权限位。
nsems:信号量数组中的信号量数量。
}

 

 

 

※// System V 消息队列,操作系统维护的进程可见的队列 --- 接口陈旧,有更好的通信方案
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
...
消息队列的相关接口
{
一.msgget
声明: int msgget(key_t key, int msgflg);
参数和shmget一样

# ipcs -q

二.msgctl
声明:int msgctl(int msqid, int cmd, struct msqid_ds *buf);

三.
消息发送
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
1.id
2.数据块的起始地址
3.数据块的大小
4.选项,一般为0,和数据块结构体中的mtype匹配

消息接收
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

数据块类型:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */ //柔性数组
};

 

 


$$$$ 信号量/信号灯 semaphore ------- SystemV版本的信号量 -- 使用成本很高
{
//前言:
所有进程都能看到的资源是公共资源/共享资源(如共享内存) -- 不安全

解决:引入互斥

互斥 :任何一个时刻都只允许一个执行流进行共享资源的访问 --//实现:加锁
临界资源:任何一个时刻都只允许一个执行流进行访问的共享资源 --- 如加了保护后的共享内存
临界区 :临界资源是通过代码访问的,所以访问临界资源的代码就是临界区. --//对临界资源保护一般是对临界区进行保护(在临界区前加锁就能保护临界资源)
同步(暂):
原子性 :要么不做,要么做完.只有两种确定状态


任何技术都有自己的应用场景:不存在放之四海而皆准的技术

信号量在多执行流并发访问临界资源时使用

场景模拟:
看电影之前,需要买票.
买票的本质功能:
1.对座位资源的预定机制
2.确保不会因为多放出去特定的作为资源,而导致冲突
如果放映厅是顶级的VIP放映厅,只有一个座位,只有一个人能预定和使用 ----> 互斥
信号量机制类似于我们看电影买票,是一种资源的预定机制..

信号量本质是一个计数器 //类似int count ...

任何一个执行流,想访问临界资源中的子资源时,不能直接访问,必须先申请信号量 -->>> count--;
只要申请成功,那么我未来就一定能拿到这个子资源,
然后能进入自己 的临界区,访问这个临界资源
当访问结束,就要释放信号量资源 -->>> count++;

.进入临界区之前先申请信号量
.退出临界区之前先释放信号量
.所有进程都得通过执行代码来申请信号量 <------- 所有进程都得先看到信号量 ---> 信号量是共享资源--->必须保证++,--操作是原子的(保护自己)


P操作:申请信号量, count--操作
V操作:释放信号量, count++操作
//PV操作都是原子性的

为什么信号量是IPC
两个进程必须要看到同一个"计数器"(资源) ---> 进程间通信IPC
//信号量不以传输数据为目的,而是以看到同一个计数器为目的

二元信号量:计数器是1的信号量 -- 互斥 -- 将临界资源独立使用

信号量相关接口
{
一.semget
声明:int semget(key_t key, int nsems, int semflg);
返回值:信号量集
参数:
nsems(信号量集):信号量的个数// n个sem的复数 //linux允许申请多个不同的信号量
//OS通过数组的方式维护信号量集
参数值为需要创建的信号量个数

二.semctl
声明:int semctl(int semid, int semnum, int cmd, ...);
参数:
1.semid:信号量集
2.semnum:对信号量集中的哪一个做操作
3.cmd ...

三.PV操作...
int semop(int semid, struct sembuf *sops, unsigned nsops);

int semtimedop(int semid, struct sembuf *sops, unsigned nsops,
struct timespec *timeout);

}

}//semaphore_end;

 


$$$$ IPC三种方式小结:
{
$ 内核中的所有IPC资源统一以数组方式管理
原理模拟演示: //原理:用于说明问题,不是真实情况
数组:struct ipc_perm* ipc_id_arr[];
三个IPC结构体:
struct shmid_ds{
struct ipc_perm perm;
XXXX;
};
struct semid_ds{
struct ipc_perm perm;
YYYY;
};
struct msqid_ds{
struct ipc_perm perm;
ZZZZ;
};
而数组struct ipc_perm* ipc_id_arr[]:
[0] --指向--->struct shmid_ds的struct ipc_perm perm //因为必须类型一样才能存,或者是结构体强转成struct ipc_perm perm类型//演示用,非实际
[1] --指向--->struct semid_ds的struct ipc_perm perm //
[2] --指向--->struct msqid_ds的struct ipc_perm perm //

而struct shmid_ds;的首地址和第一个成员struct ipc_perm perm;的首地址一样,在需要时,将数组对应元素强转就能得到整个结构体
//必须是首元素才可以,因为类型是从当前地址开始,直到最大范围.如果是第二个元素,那强转后得不到第一个元素,并且越界.

可以发现每个结构体除了第一个元素必须一样外,其他元素可以是不一样的 -------> C语言的多态

//多态并不是某个语言的特点,而是经过大量代码工程检验后发现的一种现象,总结出来的特性...


//这个数组下标是循环使用的.服务器重启或循环到起点时下标就可能出现0,1,2... . 使用长时间的话数字可能会很大,没有影响

//ipc_perm三个IPC都是一样的,内核中叫kern_ipc_perm,大同小异,有区别,但区别不大
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};

} sumup_end;

 

 

 

 

 

 

 

$$$ 信号
{
$ 信号在操作系统中已经设计好了 -- 进程能够识别(PCB)

$ 信号在任何时间都有可能产生,产生之后不一定能立即处理 --- 产生之后,处理之前会有一个时间窗口 -----> 要求进程要将信号保存起来,之后处理
$ 信号的产生对进程来讲是异步asynchronous的,--- 互不干扰
$ 同步:同步是指在多个并发执行的进程或线程之间协调其行为,以使它们能够正确地相互合作。在计算机科学中,同步通常指对共享资源进行访问控制以避免竞争条件和死锁等问题。
--- 具有协调行为
// 发送信号 ---- 时间窗口 ------ 接收信号

//信号是进程之间异步通知的一种方式,输入软中断 -- 所有的行为都是软件产生的
//硬中断:硬件产生的

$ 进程如何记录信号,记录在哪里?
--- 先描述,再组织 -- 怎么描述一个信号 --> 结构体 --- 怎么组织 ----> 数据结构


//进程需要具有记录信号的能力

// 现在的主流操作系统有两种,
1.实时操作系统 (要求高响应,如车载系统)
2.分时操作系统(linux,windows ... 基于时间片,调度器调度的O S,强调公平)

实时信号:
//实时操作系统:要求有任务要立马处理
//实时信号:要求进程立即处理
//每个信号都是个结构体对象,用队列方式管理 --- 分时操作系统中不考虑


$ 普通信号:只需要保存有无产生

$ 管理普通信号的数据结构:位图
1. 1-31个普通信号,一个整型刚好
2. 只需要保存有无,0和1可以描述
//信号位图位于PCB中:成员:signals 0|000 0000 0000 0000 0000 0000 0000 0000 //只使用了前31位,最高位待学
$ 所谓发送信号:本质是写入信号,是直接修改特定进程的信号位图 0->1

$ PCB/task_struct 只要是内核数据结构,只能由OS进行修改 --- 任何信号都要由OS来完成最后的发送过程


$ 信号处理sigaction有三种:
1.忽略此信号
2.执行该信号的默认处理动作
3.用户自定义动作:提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号

 

$ 通过指令向目标进程发送信号
# kill -l //显示所有信号
// 现象:没有0号信号,没有32和33号信号

 

$ 信号划分成两批:1-31和34-64
其中34-64是实时信号,略
1-31为普通信号,学...


$ 用户输入命令,在shell下启动一个前台进程.
//ctrl+C实际是向前台发送信号.
按ctrl+C时,键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程.
//前台进程收到信号后,执行相应动作

 

$ 前台任务:linux只允许一个终端执行一个前台任务
//键盘操作 -- 前台任务
//ctrl+C:硬件中断,信号为2 --- 证明,用signal函数重写2号方法 //SIGINT

$ 后台任务: 命令结尾带&的都是后台任务,后台任务不影响前台任务.但前台许多操作影响不到后台任务.
shell不必等待前台进程结束就可以接受新的命令,启动新的进程
SIGINT 的默认处理动作是终止进程,SIGQUIT的默认动作是终止进程并且Core Dump

//前台特征是:前台任务运行时是bash输入是无效的.
//后台特征是:bash输入有效

 

$$ 验证ctrl+c是(2号)信号
{

.使用signal接口,重定向handler函数指针到自定义方法...
现象:
1.按ctrl+C会执行自定义方法
2.kill -2 pid //也会执行自定义方法(阿里云) ------- 如果是华为云HECS的centos,可能会出现Operation not permitted(不允许操作)

结论:
1.2号信号,对进程的默认动作是终止进程
2.signal 可以进行指定的信号设定自定义处理动作
3.handler函数只有执行信号动作时才会被调用,只执行signal时并不会调用handler. --- //回调函数用例
signal只是保存了handler的地址 ----- OS内维护了默认handler函数指针数组,signal改了函数指针
}


信号的产生:
1.键盘 //ctrl+C ,CTRL+\ ...
2.系统调用 //代码..
3.指令 //shell(bash)
4.软件条件 //管道读端关闭
5.硬件 //状态寄存器,MMU...
//以上都是借助OS向目标进程发送信号/向目标进程的PCB中写信号


硬件中断原理:
1.键盘是通过硬件中断的方式,通知系统向计算机输入了数据
// 从键盘(文件系统/驱动)中读取对应的数据<-------- 中断向量表<--------- cpu寄存器 <----写入中断号------ cpu针脚 <--------发送中断号------- 硬件处理单元/中断控制器(如8259) ---- 硬件操作...
2.OS中维护了一张中断向量表,其中中断号就是中断向量表的下标.中断向量表中维护了各种中断程序和方法,用于处理各种中断(实际从键盘读取具体数据,按下键盘只是触发中断)
//一个硬件对应一个中断

软中断:OS内的信号操作是软中断

}

$ 硬件异常:
{
$ 8号信号 SIGFPE float point exception

$ 除0的本质就是硬件异常 --> CPU硬件
// 因为除0后状态寄存器由0->1,CPU读取到后,根据存放了该进程PCB的地址的寄存器,找到该进程,写入对应的信号
// 状态寄存器改变是因为除零后溢出标记位由0->1
$ 硬件异常,进程终止 的本质是CPU识别到了标志位等寄存器改变,而向进程发送信号,使进程终止

现象:使用signal捕捉SIGFPE并重定向动作后(不终止),handler会一直重复执行.
原因::因为进程没有终止.使CPU中的状态寄存器一直保持不变,导致CPU一直检测到状态寄存器异常,就一直给进程发送信号...
为什么OS也能检查出来硬件异常? 因为OS是软硬件资源的管理者,不光是管理软件,也要管理硬件

}

$ int* p == NULL
{
segmentation fault 野指针 段错误
11号信号 SIGSEGV 11 Core Invalid memory reference

*p = 100;
第一步:通过MMU寻找映射,虚拟到物理地址的转换,
报错情况
1.如果没有映射,则MMU报错
2.如果没有写权限,则MMU也报错 --- OS识别到报错信号,通过PCB地址寄存器找到PCB,给对应的PCB发送信号

//无效内存引用SEGV信号和FPE浮点异常的Action都是core , 他们的行为一样

}


$ 为什么要有那么多种信号?
1.信号的动作并不是主要的,重要的是收到什么信号,因为收到不同信号能表明出错的原因,信号种类越丰富越详细,就能根据信号来判别动作发生原因
2.进程被干掉时,不能执行完正常流程,接受不到正常退出码. 信号作用在于,在进程被干掉时,发出的信号能告诉我们进程因为什么而退出,能够表征进程是因为什么原因而异常
//正常退出通过退出码来甄别退出原因
//异常退出是通过信号来甄别退出原因


$ 进程退出码的第8位 core dump(核心转储):
{{{{


linux系统提供了一种能力,可以在一个进程在异常的时候,将核心代码部分进行核心转储,然后将内存中进程的相关数据,全部dump到磁盘中.
一般会在当前进程的运行目录下,形成core.pid这样的二进制文件(核心转储文件) //虚拟机可以直接看到,云服务器中一般看不到(云服务器默认关闭了)
# ulimit -a //显示核心信息
[chj@hecs-282967 ~ 13:56:29]$ ulimit -a
core file size (blocks, -c) 0 //核心转储文件,华为云hecs下默认为0,即大小为0,没有任何数据
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited //一个文件的大小 ,unlimited:无限的
pending signals (-i) 7267
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 65535
pipe size (512 bytes, -p) 8 //管道文件大小 512*8=4kb
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192 //一个栈的大小,
cpu time (seconds, -t) unlimited
max user processes (-u) 4096
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
//可能不太准,参考一下,只显示当前用户的 ---------------- 不同终端不同用户都不一样,每个终端都要单独设置
//似乎每次重启终端都会重置

# ulimit -c 10240 //将核心转储文件大小设置为10240kb, 即可以生成不大于10240的核心转储文件
//此后给进程发送活动为core的信号就能生成核心转储文件了

$ 核心转储有什么用?
1.方便异常后进行调试/能够更好的解决问题
# (gdb环境下) # core-file core.pid文件 //显示错误信息
{
演示:
[chj@hecs-282967 18.signal 16:54:31]$ gdb mysignal //gdb打开调试的文件
(gdb) core-file core.31402 //命令 //当前调试的文件生成的core.pid文件,必须先执行依次debug文件才能生成
[New LWP 31402]
Core was generated by `./mysignal'.
Program terminated with signal 11, Segmentation fault. //显示了异常信号名称
#0 0x0000000000400995 in main () at signal.cc:33 //显示错误行号 -- 不一定是用户想看到的文件,可能是其他文件的行号
33 *p = 10; //野指针 // 显示错误行号

描述:这种方案称为"事后调试",通过core+gdb能帮我们 自动定位问题

}


$ 为什么核心转储默认是关闭的?
前言:
云服务器默认是生产环境
一般公司分为
1.开发环境
2.测试环境
3.生产环境:对外提供服务,默认把核心转储功能关闭,因为核心转储文件很大,

举例:
在对外服务中,服务端会配有系统级监测软件,用于检测某些服务的状态...,因为系统出问题,人不一定知道或反应过来.如果出现问题会有一定自动执行的策略
而假设核心转储是打开的,如果某进程挂掉了,监测软件可能会执行重启动作,但如果是本身代码有问题,如野指针等.
则会一直挂掉,一直重启,则会不断生成核心转储文件...,会占用大量存储和性能资源

# ulimit -c 0 //关闭核心转储 core dump

作用:是否发生core dump ,0表示不发生,1表示发生
//正常退出状态core dump为0,异常时可能为1
验证:
{
核心转储关闭时,core dump是否会为1? 开启时?
1. 关闭时core dump flag不受影响,始终为0;
2.开启时,异常会置1
即:core dump开启时,如果发生异常,core dump flag会置1,并且生成core.pid文件


}}}}} core dump end;

 

 

信号接口:
{
一.signal --- //回调函数
信号捕捉
The signals SIGKILL and SIGSTOP cannot be caught or ignored.不能捕获或忽略信号SIGKILL(9)和SIGSTOP(19)。
声明:sighandler_t signal(int signum, sighandler_t handler);
参数:
1.signum 信号编号
2.handler(处理程序) 要执行的函数 --- 函数指针

二.kill //向目标进程发送对应的信号
声明:int kill(pid_t pid, int sig);
参数:
1.向pid对应的进程发送信号
2.发送哪个信号

三.raise
send a signal to the caller . 给调用者发信号. 谁调用我我给谁发,给自己发送信号
声明:int raise(int sig);
raise 函数的主要目的是让程序员能够在程序中生成信号以处理特定的情况
"raise" 函数用于引发(触发)信号,使程序能够处理特定的事件或异常情况。

四.abort
cause abnormal process termination. 导致异常进程终止
特点:即便是捕捉,重写.只要执行了abort,则一定会退出(语义),无论是否在循环... ---- 原因:abort是c语言封装的接口 --
声明:void abort(void);

一个进程只能有1个闹钟,下次会覆盖上次的

五.alarm
在seconds秒后给当前进程发送SIGALRM信号,默认动作是终止当前进程,返回值是0或上次设定闹钟还余下的秒数
声明:unsigned int alarm(unsigned int seconds);
由软件条件产生信号...
//一个进程只能设一个,有pid标识闹钟是谁设置的
$ OS中有很多闹钟,要管理,所以有
struct alarm{
int timestamp; //时间戳 --- 当前时间+设定时间 == 闹钟未来时间 == time()+设定的秒数
}
然后使用数据结构管理起来

 

}

# man 7 signal//

Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal //terminal终止
or death of controlling process
SIGINT 2 Term Interrupt from keyboard //键盘中断(暴力) ctrl+C
SIGQUIT 3 Core Quit from keyboard //退出 ctrl+\
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal //不可被自定义 //管理员信号
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process

信号处理中Action字段
term(terminate):当进程接收到 TERM 信号时,它会终止(退出)。这是一种正常的退出方式,允许进程在退出之前清理资源和保存状态。
core:当进程接收到一个信号,通常是 SIGSEGV(表示段错误)或其他导致进程崩溃的信号时,如果进程配置为生成 core 文件,它会生成一个核心转储文件,其中包含了进程在崩溃时的内存状态。这有助于开发人员分析崩溃的原因,进行调试。
Ign(ignore):当进程接收到信号时,它会忽略该信号,不采取任何特殊动作。这通常用于忽略某些信号,以保持进程的正常运行。
cont(continue):通常与 SIGSTOP 或 SIGTSTP 信号一起使用。当进程接收到 SIGSTOP 信号时,它会暂停执行,然后如果接收到 SIGCONT 信号,它会继续执行。这是一种用于暂停和继续进程执行的机制。
stop:当进程接收到 SIGSTOP 信号时,它会立即停止执行,暂时挂起。这是一种暂停进程的方式,通常由系统管理员或其他进程使用,用于暂停进程的执行,例如用于调试或管理目的。
这些动作指定了进程在接收到不同类型信号时应采取的行为。不同类型的信号可以触发不同的动作,而这些动作通常由操作系统和进程的信号处理程序来定义和控制。
//其中term和core都是终止
//trem是单纯终止,没有多余动作
//core先进行核心转储,然后再终止进程

//每个信号都有一个编号和宏定义名称
/* Signals. */
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
#define SIGILL 4 /* Illegal instruction (ANSI). */
#define SIGTRAP 5 /* Trace trap (POSIX). */
#define SIGABRT 6 /* Abort (ANSI). */
#define SIGIOT 6 /* IOT trap (4.2 BSD). */
#define SIGBUS 7 /* BUS error (4.2 BSD). */
#define SIGFPE 8 /* Floating-point exception (ANSI). */
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
#define SIGSEGV 11 /* Segmentation violation (ANSI). */
#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
#define SIGPIPE 13 /* Broken pipe (POSIX). */
#define SIGALRM 14 /* Alarm clock (POSIX). */
#define SIGTERM 15 /* Termination (ANSI). */
#define SIGSTKFLT 16 /* Stack fault. */
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
#define SIGTSTP 20 /* Keyboard stop (POSIX). */
#define SIGTTIN 21 /* Background read from tty (POSIX). */
#define SIGTTOU 22 /* Background write to tty (POSIX). */
#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
#define SIGPOLL SIGIO /* Pollable event occurred (System V). */
#define SIGIO 29 /* I/O now possible (4.2 BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGSYS 31 /* Bad system call. */
#define SIGUNUSED 31


{
在操作系统中,上下文(Context)通常指的是一个进程的状态和数据,包括所有与该进程相关的信息,以便系统可以在需要时恢复该进程的执行或切换到另一个进程。上下文包括了以下内容:

寄存器状态:这包括中央处理器(CPU)的寄存器内容,如程序计数器(PC)、栈指针(SP)、通用寄存器等。这些寄存器包含了进程的执行位置和临时数据。

内存管理信息:包括当前进程的内存分配、页表或段表等信息。这是为了确保进程能够正确地访问其内存空间。

执行上下文:这包括了有关进程的当前执行状态的信息,例如进程的优先级、状态(运行、就绪、阻塞等)、程序计数器的值等。

文件描述符和打开文件:操作系统会记录当前进程打开的文件,以便在恢复进程时可以重新建立这些文件的连接。

硬件上下文:这包括了与进程相关的硬件设备状态,如打开的输入/输出通道、网络连接等。

当操作系统需要切换到另一个进程时,它会保存当前进程的上下文,并加载新进程的上下文。这个上下文切换过程允许多个进程在共享的CPU上轮流执行,从而实现多任务处理。
上下文的保存和还原是操作系统中非常重要的功能,它允许操作系统有效地管理和调度多个进程,以便它们能够共享计算资源。
}

 

 

abort 函数和其他退出函数(如 exit、_Exit、return 等)之间有一些重要区别:
{
终止方式:

abort:abort 是一个突然的、非正常的程序终止方式。它会导致程序立即终止,不执行终止处理程序(atexit函数注册的处理程序)或关闭文件等清理工作。程序退出状态会变为非零。
exit:exit 是正常的程序终止方式。它会执行终止处理程序,关闭文件,释放资源等清理工作,并最终返回到操作系统,传递一个退出状态。这个退出状态通常为0表示正常退出,非零表示异常退出。
_Exit:_Exit 类似于 exit,但它不会执行终止处理程序,也不会刷新文件流。它用于立即退出程序,不做任何清理工作。
return:return 语句用于从函数中返回,它不是程序的退出方式。函数返回后,控制权将返回到调用函数,而不是终止整个程序。
清理工作:

abort 不执行清理工作,而其他退出函数(如 exit 和 _Exit)可以执行终止处理程序和清理工作。
return 通常用于从函数中返回,但在函数内部也可以执行清理工作,然后使用 return 返回结果。
返回状态:

abort 不提供返回状态,它总是返回非零的退出状态,表示异常终止。
exit 和 _Exit 允许您指定退出状态,以便在程序的退出状态中传递信息。
return 返回一个值,通常用于从函数中传递结果。
总之,abort 是用于强制终止程序并立即退出的函数,它不提供清理工作的机会,而其他退出函数允许程序以正常或异常退出的方式进行清理和指定退出状态。
选择使用哪个函数取决于你的需求,通常情况下,正常的程序退出应使用 exit 或 _Exit,而 abort 用于处理严重错误情况。

abort" 这个词有多种用法和含义,具体含义取决于上下文。在计算机编程和操作系统领域,"abort" 通常指的是强行终止程序的执行。在这个背景下,"abort" 表示终止或中止程序的执行,通常是由于发生严重错误或异常情况而不得不停止程序。
在这个语境下,"abort" 是一个动词,用于描述以下情况:
终止程序:abort 函数用于强制终止程序的执行,通常由程序员在代码中显式调用,或在发生严重错误时由操作系统调用。这会导致程序立即停止,不会执行后续的代码,也不会进行正常的清理工作,如关闭文件或释放资源。
异常终止:"abort" 通常用于表示程序的异常终止,这意味着程序无法继续正常执行。这可能是由于内存分配失败、访问无效的内存地址、除零错误或其他无法处理的错误情况。
核心转储:在某些情况下,"abort" 可能会导致生成核心转储文件(core dump),以便在后续进行调试,以确定程序崩溃的原因。
总之,在编程和操作系统上下文中,"abort" 表示立即终止程序的执行,通常由于严重错误或异常情况而不得不中止程序。这个术语用于描述程序的非正常终止。

}

alarm
{
alarm 的主要用途包括:

实现定时操作:你可以使用 alarm 来在一定时间间隔后执行某个操作。例如,你可以设置一个闹钟,让程序在一段时间后执行特定任务。

超时处理:在一些情况下,你可能希望程序等待某个事件的发生,但如果在一定时间内该事件没有发生,就执行某些超时处理。alarm 可以用于检测超时情况,然后执行相应的操作。

信号处理:alarm 通常与信号处理函数一起使用。你可以设置一个 SIGALRM 信号处理函数,当定时器到期时,将触发该处理函数,从而执行与信号相关的操作。

以下是一个示例,演示如何使用 alarm 来设置一个简单的定时器,以在指定时间后触发 SIGALRM 信号:

}

名词:
信号递达(Delivery):实际执行信号的处理动作
如信号的默认动作:终止,signal捕获信号后执行重定向的方法 就是递达
递达有3种动作:
1.自定义signal
2.默认SIG_DFL:终止
3.忽略SIG_IGN:接收到信号了,但什么都不做
信号未决(Pending):信号在产生后,递达前的时间窗口,称为信号未决.
阻塞(Block):进程可以选择阻塞某个信号,被阻塞的信号永远无法递达改进程(即保持未决状态),直到该阻塞被解除


$$$$ 信号的保存:
PCB维护了三张表
pending表:位图结构.比特位的位置,表示哪一个信号,比特位的内容表示是否收到该信号,1表示收到该信号,
uint_32 pengding = 0;
来信号维护公式:pengding |=(1<<(signo-1)) //减1是映射到下标.如1号是0下标,2号是1下标
block表:位图结构,比特位的位置表示那个一个信号,比特位的内容代表对应的信号是否被阻塞,为1表示该信号被屏蔽
...
handler表:是一个函数指针数组,类型为void(*)(int)的函数指针(sighandler_t),数组下标表示信号编号,数组的特定下标的内容,表示该信号的特定递达动作
// 这三张表并在一起看,一个下标三条数据

$$$ 一个进程只记录有无信号,不记录次数


SIG_ERR //(sighandler_t) -1
SIG_DEF //(sighandler_t) 0
SIG_IGN //(sighandler_t) 1
SIG_HOLD //(sighandler_t) 2 //add signal to hold mask 保持 伪装/隐藏/面具

$ 描述:description
1.每个信号都有两个标记位分别表示阻塞(block)和未决(pending),还有一个函数指针数组表示处理动作.信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清楚该标志.
如果是阻塞+未决状态,如果解除阻塞则会立即递达 --- 证明..
2.如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。
Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

// 可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称,
其正式称呼为IEEE Std 1003,而国际标准名称为ISO/IEC 9945。此标准源于一个大约开始于1985年的项目。POSIX这个名称是由理查德·斯托曼(RMS)应IEEE的要求而提议的一个易于记忆的名称。
它基本上是Portable Operating System Interface(可移植操作系统接口)的缩写,而X则表明其对Unix API的传承。


$ sigset_t
1.每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
2.因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,
阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,
未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
3.阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

$ sigset_t是一个结构体
typedef struct{
unsigned long int __val[_SIGSET_NWORDS]; //位图结构,一串long int(实际和int一样,现在长整型是longlong) 组成的数组/位图,一个整数32位一般是不够用的,所以用数组
} __sigset_t;

// #define _SIGSET_NWORDS (1024/(8*sizeof(unsigned long int))) //即 1024/(8*4) = 1024/32 = 数组大小为32 //其中8是比特数,一个字节8个比特位 //1024是需要的比特位

//假设需要寻找127所在的下标和比特位
//下标定位 : 127/(sizeof(unsigned long int)*8) = 3,即下标为3
//第几个比特位: 127%(sizeof(unsigned long int)*8) = 31
//赋值 : val[3] | (1 << 127%(sizeof(unsigned long int)*8))
//赋值 : val[3] &~(1 << 127%(sizeof(unsigned long int)*8))

$ 不建议自己使用位操作,建议使用系统提供的
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,
从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
#include<signal.h>
int sigemptyset(sigset_t *set); //全部清零
int sigfillset(sigset_t *set); //全部置1
int sigaddset(sigset_t *set,int signo); //向信号集中添加一个信号
int sigdelset(sigset_t,int signo); //从集合中删除一个信号
int sigismember(const sigset_t *set,int signo); //判断是否在集合中
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
//这些接口一般用于改变自定义的signal set,然后再通过sigprocmask去改变系统的

$ sigprocmask //sys_call
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。//即可以读取/更改 block这张位图
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask(block表)
返回值:若成功则为0,若出错则为-1
参数://不需要就设置为nullptr
1.how是怎么做,有三个参数
SIG_BLOCK:向block中添加set有的,相当于mask=mask|set
SIG_UNBLOCK:从block中移除set有的, mask=mask&~set
SIG_SETMASK:用set覆盖block, mask=set
2.set:要设置的新block集合
3.oset:old set 输出型参数,会返回老的参数,起到保存/备份效果
演示:[[[[[[[[
void showBlock(sigset_t* oset)
{
int signo =1;
for(;signo<=31;++signo)
{
if(sigismember(oset,signo)==1) std::cout<<1;
else std::cout<<0;
}
std::cout<<"\n";
}

int main()
{
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,2);
sigprocmask(SIG_SETMASK,&set,&oset);

int cnt = 0;
while(true)
{
showBlock(&oset) ;
sleep(1);
if(cnt == 10)
{
std::cout<<"recover block\n";
sigprocmask(SIG_SETMASK,&oset,&set); //如果在此之前输入2号信号,则恢复后会立即递达,即终止
showBlock(&set); //没有打印
}
cnt++;

return 0;
}

]]]]]]]]]

$ 不是所有信号都能被屏蔽,测试:1-31个信号全部添加进信号阻塞集/信号屏蔽字 set mask
结果位图:1111 1111 0111 1111 1101 1111 1111 111 //9号和19号不能被屏蔽
[[[[[[[[[[[[[[[[[[[[
int main()
{
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);

for(int signo = 1 ; signo<=31;++signo)
{
sigaddset(&set,signo);
}
sigprocmask(SIG_SETMASK,&set,&oset);
sigprocmask(SIG_SETMASK,&set,&oset); //再获取一次

while(true)
{
sigset_t pending;
int n = sigemptyset(&pending);
assert(n == 0);//一定会成功,就用assert
(void)n;
showBlock(oset) ;
sleep(1);
}
return 0;
}
]]]]]]]]]]]]]]]]]]]]

$ sigpending - examine pending signals //检查未决信号,看哪些信号处于未决状态
// 只获取未决信号位图,并不提供设置功能
#include <signal.h>
int sigpending(sigset_t *set);
description:读取当前进程的未决信号集,通过set参数传出。
返回值调用成功则返回0,出错则返回-1。
参数:set,输出型参数,返回pending信号集
演示[[[[[[[[[[[[[

void showPending(sigset_t& pending)
{
std::cout<<"当前进程的pending位图:";
int signo =1;
for(;signo<=31;++signo) {
if(sigismember(&pending,signo)==1) std::cout<<1;
else std::cout<<0;
}
std::cout<<"\n";
}

int main()
{
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,2);
sigprocmask(SIG_SETMASK,&set,&oset);

while(true)
{
sigset_t pending;
int n = sigemptyset(&pending);
assert(n == 0);//一定会成功,就用assert
(void)n;
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0 ;
}

]]]]]]]]]]]]]


$ 屏蔽2号信号后解除屏蔽,并捕获2号信号动作,让其执行自定义动作(不终止)
[[[[[[[[[[[[[
//观察未决表,0 -> 发送2号信号 -> 1 -> 10秒后解除,捕获2号信号,立刻递达 -> 0
int main()
{
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);

for(int signo = 1 ; signo<=31;++signo)
{
sigaddset(&set,signo);
}
sigprocmask(SIG_SETMASK,&set,&oset);

signal(2,handler);

int cnt=0;
while(true)
{
sigset_t pending;
int n = sigemptyset(&pending);
assert(n == 0);//一定会成功,就用assert
(void)n;
sigpending(&pending);//获取未决表
showPending(pending);//打印未决表
if(cnt++ == 10)
{
std::cout<<"解除对2号信号的屏蔽\n";
sigprocmask(SIG_SETMASK,&oset,nullptr);
}

sleep(1);
}
return 0;
}


]]]]]]]]]]]]]


$ 信号的处理 ...
1.默认
2.忽略
3.自定义动作

 

$ 信号可以立即被处理吗?
1.可以,如果一个信号之前被block,当他解除block的时候,对应的信号会立即被送达 -- block+pending
2.信号可以不是立即被处理的,而是"合适"的时候. 信号产生是异步的,当前进程可能正在做更重要的事情, -- 没有block,正在pending
当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的处理和检测
$ 什么时候从内核态切换回用户态?
1.内外部中断(包括进程时间片到了,需要切换,需要执行进程切换逻辑 )
2.系统调用返回时
3.其他系统策略返回时


用户态 User Mode
(begin)
||
||
(系统调用 ┎\/
或中断) | (续)<-----------------------------┒ ┎--------------------> 执行handler -----┒
| || | | |
| || | | |
| \/(end) ⇧ ⇧ ⇩ (执行sigreturn函数回到内核态)
---------------+---------------------------------------+-----+-----------------------------------------+--------------
| \ / | (回到信号处理)
内核态 | (没信号了,直接回到正常执行流) \/ (如果该信号是自定义动作,要切换到用户态) |
Kernel Mode | 检查当前进程的三张表 |
| /\ | /\ |
⇩ || | || (回到信号处理) |
完成系统任务 ------------------------------┙ | ┗---------------------------------------┙
|
┗---------->-- 陷入内核
信号三张表,三张uint32_t位图
1.block
2.pending
3.handler

handler是用户态执行的,
1.溯源,用户操作痕迹
2,受权限约束,事故影响小

$ 每次从内核态到用户态都会进行一次信号检测

$ 执行自定义动作的过程
// sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。
// sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态.
如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。 --- 那如果还有其他信号,则取决于OS策略,可能继续处理,可能是下次处理...

$ 为什么要在不能在内核态执行handler?
1.防止用户在handler方法中做了非法操作,影响OS安全和稳定 --- 阻止或不轻易在内核态中执行用户代码
2.进程用户态时会受到权限约束,影响有限
3.溯源,linux中有用户操作痕迹

$ 为什么执行完handler后必须回到内核态? 处于用户态时,用户不知道中断的位置等,因为这些工作是由操作系统做的

$ 只有内核态才能检测信号,因为信号位于PCB中,PCB只有OS在维护

$ 执行信号动作前,会先将pending表清零,再执行

 

$$ sigaction --- examine and change a signal action --- 能够处理实时信号,后续...
声明:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:

1.act:输入型参数,是一个和函数同名的结构体
2.oldact:输出型参数

struct sigaction {
void (*sa_handler)(int); // 和signal的handler一样
void (*sa_sigaction)(int, siginfo_t *, void *); //实时
sigset_t sa_mask; //信号集类型,位图
int sa_flags; //置0 -- 实时
void (*sa_restorer)(void); // 实时
};
// 部分信号给实时信号用的,置0即可 -- 目前只需使用 handler和mask

1. 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,
这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
2.如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

//演示进程收到信号时,,pending清零,sigaction阻塞其他信号
[[[[[[[[[[
static void showPending(sigset_t& pending)
{
std::cout<<"当前进程的pending位图:";
int signo =1;
for(;signo<=31;++signo) {
if(sigismember(&pending,signo)==1) std::cout<<1;
else std::cout<<0;
}
std::cout<<"\n";
}

static void handler(int signu)
{
std::cout<<"get a signal: " <<signu <<"pid: "<<getpid()<<std::endl;
int cnt = 10;
while(cnt)
{
cnt--;

sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
showPending(pending); //刚进去时,循环内第二次信号达到前,2号pending是0,说明是执行信号动作时,是先清零再执行
//当在循环内第二次到达后,pending2号立刻变为1,说明2号信号此时被block,无法被立即处理.因为当前循环是正在处理2号的handler内
//当handler循环结束后,会立刻捕捉 block且pending中 的2号,再次清零pending,然后执行handler,
sleep(1);
}
}

int main()
{

struct sigaction act,oldact;
memset(&act,0,sizeof(act));
memset(&oldact,0,sizeof(oldact));
act.sa_handler = handler;
act.sa_flags = 0;
//以上等价于signa

sigemptyset(&act.sa_mask);

//处理sigaction中的signo号的同时阻塞3,4,5号信号
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);

sigaction(2,&act,&oldact);

while(true) sleep(1); //1.ctrl+C发送2号信号,转到handler

return 0 ;
}


]]]]]]]]]]]]]]]]]

 

 


$$$$ 子进程退出了,父进程如何得知?
已学过:父进程阻塞式等待 && 非阻塞(轮询式) --- 需要父进程主动检测. --- 子进程退出时,父进程知道吗?

$ 子进程在退出时,会给父进程发送SIG_CHLD信号. -- SIG_CHLD,17号 --->只不过这个信号的默认动作是什么都不做

新方法:信号 --- 当父进程很忙时,可以选择下面这种方法执行 //但父进程没什么事干的时候,直接wait就好
//但这种方法有问题,他仅适合一个子进程.当进程很多时,就不一定能满足了,因为一个进程只能记录有没有信号,而不能记录有多少个信号
[[[[[[[[[[[[[
pid_t id = 0;

void handler(int signo)
{
printf("捕捉到一个信号: %d,who: %d\n",signo,getpid());
sleep(5);
pid_t res = waitpid(-1,nullptr,0);//返回等到的子进程 的pid //-1代表回收任意的子进程
if(res>0)
{
printf("wait success, res: %d, id: %d\n",res,id);// 比较res和id是否相同
}
//exit(1);
}

int main()
{

signal(SIGCHLD,handler);

id = fork();
if(id==0) {
int cnt = 5;
while(cnt--){
std::cout<<"我是子进程,我的pid是:"<<getpid()<<" ppid: "<<getppid()<<"\n";
sleep(1);
}
exit(1);
}

]]]]]]]]]]]]]

[[[ 错误代码演示
pid_t id = 0;

void handler(int signo) //时间很短,一次有10个子进程发出信号,而父进程一次只能接受一个,第二个处于阻塞,其他的就覆盖掉了 --
---- 因为一个进程只能记录有没有信号,而不能记录信号的个数
{
printf("捕捉到一个信号: %d,who: %d\n", signo, getpid());
sleep(5);
pid_t res = waitpid(-1, nullptr, 0);
if (res > 0)
{
printf("wait success, res: %d, id: %d\n", res, id);
}
}

int main()
{
signal(SIGCHLD, handler);
int i = 1;
for (; i <= 10; ++i)
{
id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt--)
{
std::cout << "我是子进程,我的pid是:" << getpid() << " ppid:" << getppid() << "\n";
sleep(1);
}
exit(1);
}
}
while (1)
{
sleep(1); //父进程正在忙....
}
return 0;
}

]]]


//如果全部子进程一次全部退出
//问题:如果只有一部分子进程退出,即不是所有的子进程都退出,那waitpid会一直等待,阻塞式,导致代码无法返回
[[[[
void handler(int signo)
{
printf("捕捉到一个信号: %d,who: %d\n", signo, getpid());
sleep(5);
while (1) //多了一个循环,一次性全部干掉,不知道有多少个进程需要退出
{
pid_t res = waitpid(-1, nullptr, 0); // 返回等到的子进程 的pid
if (res > 0)
{
printf("wait success, res: %d, id: %d\n", res, id); // res和id是否相同
}
else
break; //循环至没有子进程zombie
}
printf("handler done.\n");
}

]]]]

//基本完善版 --- 基于信号的回收子进程 函数/方法
[[[
pid_t id = 0;
void waitProcess(int signo)
{
printf("捕捉到一个信号: %d,who: %d\n", signo, getpid());
sleep(5);
while (1)
{
pid_t res = waitpid(-1, nullptr, WNOHANG); // 返回等到的子进程 的pid,非阻塞,保证进程正常执行
if (res > 0)
{
printf("wait success, res: %d, id: %d\n", res, id); // res和id是否相同
}
else
break; //循环至没有子进程zombie
}
printf("handler done.\n");
}
]]]

 

$$$$$$$
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,
这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,
但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
[[[
pid_t id = 0;

int main()
{
// signal(SIGCHLD, waitProcess);
signal(SIGCHLD,SIG_IGN);

int i = 1;
for (; i <= 10; ++i)
{
id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt--)
{
std::cout << "我是子进程,我的pid是:" << getpid() << " ppid:" << getppid() << "\n";
sleep(1);
}
exit(1);
}
}

while (1) sleep(1);

return 0;
}

]]]

$ 子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略.但为什么还需要手动signal(SIGCHLD,SIG_IGN)呢? -- 是我们混淆了
--- 其实是17号SIGCHLD的SIG_DEF是什么都不做,而不是默认SIG_IGN. -- 即子进程发送SIGCHLD时,默认动作时SIG_DEF,而不是SIG_IGN

$ 实际上,signal(SIGCHLD,SIG_IGN)是需要我们手动设置的:
当signal检测到(SIGCHLD,SIG_IGN)这种组合时,会检测父进程PCB的某状态标志位或状态位,父进程fork创建子进程时,子进程会把状态位继承下去,
说明凡是父进程创建的,以后都不用僵尸了,当子进程退出时,OS会检测标志位,如果设置过了就能直接释放掉
//这种做法比自定义wait更优雅,方便

 

 


- - - - - - - - - - - -


$$$$ 可重入函数/不可重入函数: --- 不同的执行流中同一个函数被重复进入
//重复进入
/*
{
可重入函数:如果一个函数同时进入多个线程而没有安全问题,则是可重入函数
不可重入函数:多线程会有安全问题..

1.大部分函数是不可重入函数
2.一般只有全部是局部变量的函数才是可重入函数
3.如SLT里大部分是不可重入函数,很多是new或malloc出来的.
4.使用了cout等的也是不可重入函数,因为文件本身就是共享资源

示例描述:
// main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因
为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函 数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从
sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步
之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只
有一个节点真正插入链表中了。
insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称
为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,
如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

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

---- main 和 信号 两套不同的执行流
}

*/


$$$$$$$$$$$$$$$$$$$$$$$$$ 多线程

注意:单核机器和多核机器现象可能不同,多使用不同机器观察


###什么是线程
1.在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
2.一切进程至少都有一个执行线程
3.线程在进程内部运行,本质是在进程地址空间内运行
4.在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
5.透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

###线程的优点
.创建一个新线程的代价要比创建一个新进程小得多
.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
.线程占用的资源要比进程少很多
.能充分利用多处理器的可并行数量
.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

###线程的缺点
.性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
.健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
.缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
.编程难度提高
编写与调试一个多线程程序比单线程程序困难得多

###线程异常
.单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
.线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该
.进程内的所有线程也就随即退出

线程用途
.合理的使用多线程,能提高CPU密集型程序的执行效率
.合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

##2. Linux进程VS线程
###进程和线程
.进程是资源分配的基本单位
.线程是调度的基本单位
.线程共享进程数据,但也拥有自己的一部分数据:
> 线程ID
> 一组寄存器
> 栈
> errno
> 信号屏蔽字
> 调度优先级

// 最重要的区别是: 线程各自的寄存器组(上下文切换)和线程独立栈(独立运行)

 

 


###进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
> 文件描述符表
> 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
> 当前工作目录
> 用户id和组id

###错误检查:
.pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
.pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

###NPTL (NATIVE POSIX Thread Library) //本地POSIX线程库

$$ 线程:
//执行流

$ 进程地址空间进一步:
{ |---------------------------------------------------------进程------------------------------------
| OS
CPU ---------------------------+--------> task_struct ------------------> 进程地址空间 <-------------------> 页表 <---------------------> 物理内存空间 ------------------------------------------- 磁盘
寄存器:指向PCB -----------¹ | TCB |---------------| |--------------|----------------┒ |--------------|-
寄存器:指向用户级页表 --->... | TCB | 内核空间 | 32位使用2级页表 | 4KB ---+----页框/页帧 | | 4KB ---+-
寄存器:指向内核级页表 --->... | ... |---------------| |--------------| | |--------------|
寄存器IR: | | | 64位使用3级页表 | | | | |
保存当前执行的指令 | | | |--------------| | |--------------|
寄存器PC: | | 主线程栈 | | |---------------------- 页( page ) | |-
下一条要执行的指令 | |---------------| ┎动态库 libpthre |--------------| |--------------|
寄存器esp:指向栈顶 | | nmap区 ├|struct pthread | | | |
寄存器ebp:指向栈底(栈帧) | | | |线程局部存储 |--------------| |--------------|
寄存器: | |---------------| ┗线程栈 | | | |
保存经常要访问的高频数据 | | | |--------------| |--------------|
寄存器:保存上下文... | | | | | | |
... | | 堆 | |--------------| |--------------|
不可见寄存器: | |---------------| | | | |
(有些寄存器是OS不可见的.) | | 未初始化数据段| |--------------| |--------------|
| |---------------| | | | |
| | 已初始化数据段| 全局变量/static变量 |--------------| |--------------|
运算器 | |---------------| | | | |
控制器 | | 代码段 | |--------------| |--------------|
硬件cache L1,L2,L3: | |---------------|
局部性原理,提前缓存 |--------------------------------------------------------------------------------------------------------
--- 现代计算机预加载数据的理论基础

}

$ 可见寄存器/不可见寄存器
有些寄存器是OS不可见的.
有些寄存器是暴露给程序员进行系统开发或内核开发,由CPU内部自己做管理的

$ 只要修改esp,ebp就能实现切换栈 --- 在不同的栈中开辟相同的变量(同名但内容不同的变量)
语言上开辟空间是汇编上如esp-4(int32),esp-8(int64)... //是一串汇编代码

$ 首地址是地址最低的地址,栈是向 下/低地址 开辟的, 变量是向 上/高地址 存放的.
开辟栈空间是栈顶-偏移量,读取变量空间是首地址+偏移量:start+sizeof(type)

$ -- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址


概念:
教材观点:
1.线程是一个执行分支,执行粒度比进程更细,调度成本更低.
2.线程是进程内部的一个执行流

线程与进程区分
CPU分不清自己调度的是进程还是线程,也不需要区分 ---
切换PCB就切换了进程,就像火车头,切换车厢(进程),牵哪个车厢就跑哪个进程


线程:创建多个PCB,都指向同一个进程地址空间,分别执行同一个进程的不同区域的方法(执行粒度更细).这些在进程的PCB就是线程
让串行执行变成并发执行,提高效率

所谓线程是进程内部的一个执行分支,即线程在进程的地址空间内运行

切换的工作是谁做的呢? 是CPU执行OS的代码完成的,即是OS做的
OS发现线程的PCB的地址和页表等和进程是一样的,就不做切换了


调度成本更低:
1.页表和地址空间不用换(次)
2.不刷新缓存(局部性原理),在进程内线程间切换时,缓存不用刷新(主)
--- 切换进程时,cache缓存会失效,需要重新加载

$ 现在,什么是进程? 进程包括了内部的执行流,包括地址空间,页表,还包含对应的代码和数据(内存),这些合起来才是进程
--- 进程是承担分配系统资源的基本实体
--- 进制至少要有1个PCB,一个地址空间,一套页表,和对应的代码和数据
$ 即操作系统分配资源是按照进程为基本单位,以进程为载体来分配的,只有有了进程,才能分配线程
线程需要向进程申请资源
$ 把程序从磁盘加载到内存形成进程 === 让操作系统为该进程申请匹配的所有资源

$ 之前学的进程是只有一个task_struct的进程

$ CPU调度的基本单位是线程
--- CPU只认PCB,

---------------------------------------------------------
操作系统线程概念:
$ 并不是所有的OS都是执行上面一套方案的
不同的操作系统有不同的执行方案
任何一款操作系统都有线程和进程,有很多进程,所以要把进程管理起来:描述+组织
线程在进程内运行 --> 线程个数比进程更多 --> OS要管理线程 -->线程数据结构TCB Thread Control Block

$ TCB:线程控制块,属于进程PCB,可以有属于线程的调度方案(也可以没有)

//windows内有真线程 --- 非常复杂

//linux是复用了PCB的结构体,用PCB模拟线程的TCB --- 根据PCB和TCB有较高的相似性,linux很好的复用了进程的设计方案
--- 所以linux没有真正意义上的线程,而是采用进程方案模拟的线程.(非真线程)
--- 复用代码和结构(顶级设计方案),使操作更加简单 --- 好维护,效率更高,更安全 ---> 让linux可以长时间不间断的运行

$ 实际上,一款OS,使用最频繁的功能,除了OS本身,就是进程最多.
如果进程方案设计的很臃肿,很不好维护的话,则很容易出问题,效率可能还不高(数据结构复杂)

$ Linux把历史的进程,线程等称为轻量级进程LWP Lightweight processes
--- linux中线程不分父子,分为主次,主线程,次线程,他们的PID都是一样的 --- 说明是同一个进程内的
---

$ 线程库:pthread
头文件:<pthread>


# ps -aL //
LWP 是轻量级线程的缩写,表示线程的编号,OS识别LWP和PID一样时,说明是主线程,不一样说明是次线程.
如果PID和当前进程一样时,说明是当前进程.否则说明是跨进程调度

// 为什么块大小4kb? 局部性原理,提高IO效率,加快计算机速度

 

多线程程序中,任何一个线程奔溃了,最后都会使进程奔溃
1.系统角度:线程是进程的执行分支,线程奔溃就是进程奔溃
2.信号角度:linux信号是以进程为主的,收到信号会处理进程


先有虚拟内存和地址空间,才把可执行程序的代码和数据加载到内存,全部加载和局部加载由OS自主决定,缺页中断

$ 线程共享进程数据,但也拥有自己的一部分数据
1.线程ID,linux中叫LWP

* 2.一组寄存器:线程的执行和切换的上下文数据 -- 体现切换特性
* 3.栈 -- 独立的栈 -- 体现独立运行特性

4.errno
5.信号屏蔽字
6.调度优先级

linux下没有真正意义的线程,而是进程模拟的线程(LWP) -- 所以linux不会提供直接创建系统线程的系统调用,只提供创建轻量级进程的接口
为了给用户提供线程,linux提供了库,将linux接口封装,用于给对用户提供进行线程控制的接口 --- 用户级线程库 --- 用户级多线程
//用户级线程库实现的线程称为用户级多线程


库名: pthread.h --- 所有linux都自带 --- 这种系统必带的库称为 原生线程库
p=POSIX,pthread=POSIX thread

 

$ 线程接口
{
一.创建进程
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg );
注意:Compile and link with -pthread . 编译和链接时带上pthread库
参数:
1.pthread_t:unsigned long int
thread:线程id,输出型参数,创建线程后会返回
2.pthread_attr:
union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
};
attr:线程的属性,一般设为NULL就可以了
3.start_routine:返回值为void*,参数为void*的函数指针.用于执行我们分配给线程执行的方法,执行完pthread_create所有工作后,才回调routine
4.arg:函数指针start_routine的参数,void可以接受任意类型
返回值:
成功返回0,错误返回错误码

错误:
undefined reference to `pthread_create'
原因:pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.so,所以在使用pthread_create()创建线程,需要链接该库。
解决:#在编译中要加 -lpthread参数
# gcc thread.c -o thread -lpthread

}

$ Linux线程调度和进程一样,运行谁由调度器决定

---------------------------------------------------------------------- 线程使用基本演示,创建线程
//此时10个线程都在重入这个函数
void* thread_run(void* args)//void可以接受任意类型
{
char* name = (char*) args;
while(1){
printf("new thread running,my thread name is: %s\n",name);
sleep(1);
}
delete[] name;
return nullptr;
}

int main()
{
pthread_t tid[NUM];
for(int i = 0; i<NUM;++i){
char* tnmae = new char[64]; //解决方法1,开辟独立的堆空间 -- 相当于给每个线程都分配了独立的堆空间
//char tname[64]; //栈空间循环可能不会销毁,或者可能是会重复利用.因此栈内开辟的临时数组传参有被覆盖的可能
snprintf(tname, sizeof(tname),"thread-%d",i);
pthread_create(tid+i,nullptr,thread_run,tname);
}


//return 0; //如果主线程退出,则所有线程也退出

while(1){
printf("main thread running\n");
sleep(1);

}

return 0;
}
---------------------------------------------------------------------- 创建线程及基本演示___end


###线程等待 为什么需要线程等待?
.已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
.创建新的线程不会复用刚才退出线程的地址空间。

###调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

$ 线程也有类似僵尸进程的问题,必须要主线程去等待新线程退出 --- 需要新线程全部退出后,主线程才能退出

接口:pthread_join -- 等待线程
int pthread_join(pthread_t thread, void **retval); //默认是阻塞等待
返回值:成功返回0
功能:
1.join在等待时,会检测线程属性是否被joinable,如果是joinable,则阻塞或非阻塞等待... ,如果不是,则会得到得到errno

 

 

问题:
如果线程自己分离,且如果主线程比线程先调度,且是阻塞等待,则可能不会错误.
因为哪个线程先运行是不确定的,如果主线程先运行,则能直接挂起等待新线程,如果挂起期间新线程主动分离自己,主线程是不知道的,就有可能一直等待下去


---------------------------------------------------------------------- 等待线程时未检测到非joinable就进入阻塞
void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
std::string name = static_cast<const char*>(args);
int cnt = 5;
while(cnt)
{
std::cout<<name<<" : "<<cnt--<<std::endl;
sleep(1);
}
return nullptr;
}

int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

//sleep(1); //等待一秒,收到线程已分离. 不等待则进入阻塞
int n = pthread_join(tid,nullptr);
if(n !=0)
{
std::cout<<"error"<<n<<" : " << strerror(n) << std::endl;
}

std::cout << "all thread quit" << std::endl;

return 0;
}

#############################
不等待(主线程比次线程调度快)结果:
[chj@hecs-282967 2thread 22:28:58]$ ./thread
thread 1 : 5
thread 1 : 4
thread 1 : 3
thread 1 : 2
thread 1 : 1
all thread quit
说明:阻塞等待只会检查1次,如果没有检查到,会阻塞直到次线程结束(!!!)

主线程等待1s后结果
[chj@hecs-282967 2thread 22:28:29]$ ./thread
thread 1 : 5
error22 : Invalid argument
all thread quit
---------------------------------------------------------------------- 等待线程时未检测到非joinable就进入阻塞___End

 

 

线程终止三种情况
1.线程函数执行完毕
//exit是进程退出,不是进程退出,如果在线程内执行,则整个进程(包括所有线程)直接退出

2.调用函数pthread_exit


pthread_exit -- terminate calling thread. 终止调用该函数的线程 --- 线程自己退,几乎等价return
声明:void pthread_exit(void *retval);
返回值:无
参数:
The pthread_exit() function terminates the calling thread and returns a value via retval
that (if the thread is joinable) is available to another thread in the same process that calls pthread_join(3).
pthread_exit()函数终止调用它的线程,并通过retval返回一个值,
如果线程是可被等待(joinable),那么这个值将在同一进程中的另一个线程调用pthread_join(3)时可用。

$ 线程只有退出码,没有退出信号.
因为线程异常收到信号是直接发给进程,从进程层面处理进程.所以线程不考虑异常,只考虑正常


---------------------------------------------------------------------- 线程:使用基本类型参数做返回值
void* thread_run(void* args)//void可以接受任意类型
{
char* name = (char*) args;
while(1){
printf("new thread running,my thread name is: %s\n",name);
break;
}
delete[] name;
sleep(1);
pthread_exit((void*)1);
return nullptr;
}

int main()
{
pthread_t tids[NUM];
for(int i = 0; i<NUM;++i){
char* tname = new char[64];
//char tname[64];
snprintf(tname, 64,"thread-%d",i+1);
pthread_create(tids+i,nullptr,thread_run,tname);
}

void* retval ;

for(int i = 0 ; i<NUM ; ++i){
int n = pthread_join(tids[i],&retval);
if(n!=0){
std::cout<<"phread_join error"<<std::endl;
}
std::cout<<"thread ret:"<<(uint64_t)retval <<"\n"; //
//云服务器centos64位,指针是8字节,int是4字节,如果直接int会发生精度截断而警告,为了不发生精度截断,所以uint64,

std::cout<<"all thread quit"<<std::endl;

return 0;
}
---------------------------------------------------------------------- 线程:使用基本类型参数做返回值___end

---------------------------------------------------------------------- 线程:使用对象作返回值
#define NUM 10

enum
{
OK = 0,
ERR
};

// 不只是可以传整型,还可以传类,传更多信息,让线程去完成
class ThreadData
{
public:
ThreadData(const std::string &name, int id, time_t createTime, int top)
: _name(name), _id(id), _createTime((uint64_t)createTime), _status(OK), _top(top), _result(0)
{
}

public:
std::string _name;
int _id;
uint64_t _createTime;

// 要返回的数据,方式1
int _status; // 退出码
int _top; // 最大值
int _result;
// char arr[n] //如果需要线程排序,可以传数组...
};

// 也可以定义一个专门返回数据的类,方式二
// class ResultData{};

void *thread_run(void *args) // void* 可以接受任意类型,返回值和参数匹配,目的是使用同样的参数输入和输出
{
ThreadData *td = static_cast<ThreadData *>(args);
for (int i = 1; i <= td->_top; i++)
{
td->_result += i;
}
std::cout << td->_name << " cal done!" << std::endl;

// sleep(1);
// pthread_exit(td); //return和texit(pthread)返回效果一样,都是输出型参数
return td;
}

int main()
{
pthread_t tids[NUM];
for (int i = 0; i < NUM; ++i)
{
// char* tname = new char[64];//这时就不再new了,因为new了还要另外回收
char tname[64];
snprintf(tname, 64, "thread-%d", i + 1);
ThreadData *td = new ThreadData(tname, i + 1, time(nullptr), 100 + 5 * i); // 让线程做求和
pthread_create(tids + i, nullptr, thread_run, (void*)td);
sleep(1);
}

void *retval = nullptr; // 用于接收线程任务的"返回值"--- return和t_exit的

for (int i = 0; i < NUM; ++i)
{
int n = pthread_join(tids[i], &retval);
if (n != 0)
{
std::cout << "phread_join error" << std::endl;
}
ThreadData *td = static_cast<ThreadData *>(retval); //???
if (td->_status == OK)
{
std::cout << td->_name
<< "计算结果是:" << td->_result << ", 它要计算的是[1," << td->_top << "]的和"
<< "\n"; //
}
}

std::cout << "all thread quit" << std::endl;
return 0;
}
---------------------------------------------------------------------- 线程:使用对象作返回值___end

 

 

$ 接口:pthread_cancel --- send a cancellation request to a thread 主线程给线程发送取消请求,主线程让新线程退
{取消线程 -- 使用频率不高

测试:
// 自己能取消自己吗? //可以
// 新线程能取消主线程吗? //不能,没有效果

退出码:PTHREAD_CANCELED #define PTHREAD_CANCELED ((void *) -1) //被取消的线程的返回值是-1
}

 

 

 

 

$ 接口 pthread_self -- 线程获取自己的id
{接口:pthread_self -- obtain ID of the calling thread 获取调用线程的ID
声明: pthread_t pthread_self(void);
返回值:返回自己的ID
}
---------------------------------------------------------------------- 线程退出,获取自己id演示
void *threadRun(void* args)
{
const char*name = static_cast<const char *>(args);

int cnt = 5;
while(cnt)
{
cout << name << " is running: " << cnt-- << " obtain self id: " << pthread_self() << endl; ---- 获取自己ID
sleep(1);
}

pthread_exit((void*)11);

// PTHREAD_CANCELED; #define PTHREAD_CANCELED ((void *) -1)
}

int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
// sleep(3);

// pthread_cancel(tid); // --- 线程退出

void *ret = nullptr;
pthread_join(tid, &ret);
cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl; ---- 打印线程id比对
return 0;
}
---------------------------------------------------------------------- 线程退出演示___end

 

 

 

 

? 能否非阻塞等待线程,如何非阻塞等待线程? -- 分离线程
? 为什么线程ID这么大,用来干什么的? -- pthread_t就是一个地址数据,用来标识线程相关属性集合的起始地址,默认打出来是10进制,比较大
//虚拟地址

? 什么叫做线程ID,线程ID和LWP的关系是什么? 基本没关系,LWP用户标识用

? 什么是线程分离

什么是用户级线程库 /lib64/libpthread-2.17.so
[chj@hecs-282967 2thread 22:59:14]$ ls /lib64/libpthread*
/lib64/libpthread-2.17.so /lib64/libpthread.so
/lib64/libpthread_nonshared.a /lib64/libpthread.so.0
线程也受线程库中的代码管理
为什么库也要管理线程? 因为linux没有真正的线程,只有轻量级进程,最多也只能管理轻量级进程.所以库也要承担一部分管理线程的工作
库如何管理线程? 创建类似的管理线程的TCB -- 用户级属性 --- 类似C语言的struct FILE文件指针,C++的class fstream文件流对OS的struct file结构体的封装
┎ ...
| 动态库 libpthread-2.17.so
| struct pthread <----- pthread_t id:每个线程结构体的起始地址,即线程ID <== 为什么tid是一串很长的数字的原因
mmap区: | 线程局部存储 //线程局部变量的存储空间,一般只能存内置类型,不能存自定义类型.有什么用:如全局变量很少使用
| 线程栈 //线程的栈空间 ,和主线程栈的功能差不多
| //每个管理线程的描述结构体如数组似紧挨着
| struct pthread <----- pthread_t id
| ...

(mmap区是共享区)

//线程局部存储Thread Local Storage,缩写为TLS
从全局变量的角度上来看,就好像一个全局变量被克隆成了多份副本,而每一份副本都可以被一个线程独立地改变。
$ 与全局变量和线程局部变量的区别:
全局变量是所有线程共享.
线程局部变量(位于线程栈)是除主线程外的新线程自己用自己的.
TLS是包括主线程在内的所有线程都有,并且独立

 

用户级线程库与系统接口: //pthread封装了clone等的系统调用
int clone(
int (*fn)(void *) --- 用户提供的方法/函数
,void *child_stack --- 库提供的栈
,int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );


$ C++提供的<thread>库
-- 即使包含了C++提供的线程库<thread>,仍需要带-lpthread , 因为thread也是用了pthread提供的接口做的封装
//所以linux 下编译前准备工作还是很多的


? 使用语言的还是系统提供的库?
1.推荐使用语言的,因为语言支持的跨平台能力更好.除非是确定锁定某个平台开发

? 线程的私有栈是什么? --- 叫独立栈更准确
线程的私有栈使用的是库提供的栈,位于共享区.
主线程的栈使用的是系统的进程空间上的栈

 

线程寄存器\栈可以是各自独立一份的,其他的如堆,静态区(全局变量),常量区都是共享的

 

? 线程私有数据
1.全局变量所有线程共享
2.局部变量各线程独立 -- 实现方式:在自己的栈上定义(通过 切换ebp,esp寄存器,然后esp-4得到地址,这样的方式来得到独立的变量) --- :切换栈方式实现地址偏移/偏移量


$ 就算是独立栈,独立数据,其他线程想看也是能够看得到。因为数据都在同一个进程空间.

 

---------------------------------------------------------------------- 线程局部存储演示:

__thread int g_val = 100;//gcc/g++的选项,使全局变量给每个线程都拷贝一份,是属于线程内部的全局变量
//只支持内置类型,语言级别的类型,类不支持
//只类似主线程的全局变量,不能是static,因为static一定在全局已初始化数据区

 

// int g_val = 100;//所有线程共享

std::string hexAddr(pthread_t tid)
{
g_val++; //作用:统计该函数被该线程调用了多少次
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%lx", tid);
return buffer;
}

void *threadRoutine(void* args)
{
// static int a = 10;
std::string name = static_cast<const char*>(args);
int cnt = 5;
while(cnt)
{
// cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << " &cnt: " << &cnt << endl;
std::cout << name << " g_val: " << g_val++ << ", &g_val: " << &g_val << std::endl;
sleep(1);
}
return nullptr;
}

int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1"); // 线程被创建的时候,谁先执行不确定!
pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2"); // 线程被创建的时候,谁先执行不确定!
pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3"); // 线程被创建的时候,谁先执行不确定!

pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);

return 0;
}
---------------------------------------------------------------------- 线程局部存储演示___End

1.分离线程:
接口:pthread_detach --- detach a thread
声明:int pthread_detach(pthread_t thread);
有什么用?

###. 分离线程
.默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
.如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

1. 如果一个线程被分离,则无法在被join. --- 分离后join的现象:主线程收到error信息,次线程无影响(不知情)
2.一个线程默认创建出来就是joinable的.如果把属性设置成分离,则无法在被join,再次join会报错 ---- 线程也有属性
3.分离线程可以自己分离,也可以别人分离.
4.分离线程前必须是未分离状态

---------------------------------------------------------------------- 分离线程演示:自己分离自己
void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // ---复合函数实现自己分离自己
std::string name = static_cast<const char*>(args);
int cnt = 5;
while(cnt)
{
std::cout<<name<<" : "<<cnt--<<std::endl;
sleep(1);
}
return nullptr;
}

int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
sleep(10);

//pthread_detach(tid); //主线程分离

std::cout << "all thread quit" << std::endl;

return 0;
}
---------------------------------------------------------------------- 分离线程演示:自己分离自己___End

 

 

 

互斥 :任何一个时刻都只允许一个执行流进行共享资源的访问 --//实现:加锁
临界资源:任何一个时刻都只允许一个执行流进行访问的共享资源 --- 如加了保护后的共享内存
//临界资源:对共享资源进行一定的保护
临界区 :临界资源是通过代码访问的,所以访问临界资源的代码就是临界区. --//对临界资源保护一般是对临界区进行保护(在临界区前加锁就能保护临界资源)
//非临界区:不访问临界资源的代码
同步(暂):
原子性 :要么不做,要么做完.只有两种确定状态

$$ 互斥
1.只要是共享,就会存在并发访问问题
如对全局变量做修改时,如果没有保护,会有并发访问问题,进而导致数据不一致问题 ---- 数据不一致问题
2.串行访问,上锁,独享:互斥

一条C语言修改代码,至少要3条汇编指令1.读取,2.修改,3.写回
让这3条语句看起来像一条一样执行,要么不做,要么做完 --- 原子性

---------------------------------------------------------------------- 多线程访问共享资源场景
int tickets = 10000;

void* threadRoutine(void *name)
{
string tname = static_cast<const char*>(name);

while(true)
{
if(tickets>0)
{
usleep(2000);
cout<<name<<" get a ticket: "<<tickets--<<endl;
}
else
{
break;
}
}
return nullptr;
}

int main()
{
pthread_t t[4];
for(int i = 0 ; i<4; i++)
{
char* tname = new char[64];//C语言接口,传C字符串
snprintf(tname,64,"tname-%d",i+1);
pthread_create(t+i,nullptr,threadRoutine,tname);
}

for(int i = 0 ; i < 5; i++)
{
pthread_join(t[i],nullptr);
}

return 0;
}
结果:
....
0x173cc20 get a ticket: 3
0x173cec0 get a ticket: 2
0x173d400 get a ticket: 1
0x173d160 get a ticket: 0
0x173cc20 get a ticket: -1
0x173cec0 get a ticket: -2
Segmentation fault
---------------------------------------------------------------------- 多线程访问共享资源场景___End

//打印窗口出现多线程并发的原因:显示器也是共享资源(文件),需要加锁保护


锁接口
{
一:创建,初始化,销毁

pthread_mutex_t mutex; //定义一个锁,有多种定义方式

1.int pthread_mutex_destroy(pthread_mutex_t *mutex);
作用:对锁进行销毁

###销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

2.int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
作用:对锁进行初始化,使锁进入可工作状态
参数:
a.mutex:输出型参数,对该锁作初始化
b.attr:(设为null)

3. pthread_mutex_t mutex; //局部锁,必须要init和destroy

# pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义一个互斥锁 -- 必须是全局的 -- 使用PTHREA_MUTEX_INITIALIZER做全局初始化,并且可以销毁
//不需要pthread_mutex_init和pthread_mutex_destroy; 因为全局锁本来就没必要销毁,等进程结束系统自动回收即可

类型pthread_mutex_t:互斥锁

二.使用锁:上锁,解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
作用:加锁,加锁成功后,就能访问临界资源. 如果加锁失败,则进入阻塞状态)(说明有资源有人在使用)

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);
作用:解锁

}

$ 实际使用锁时,线程竞争锁的能力是会有区别的.执行运算很快的任务的线程竞争锁的能力特别强,可能会导致其他线程无法获得锁,所以需要合理分配任务
(有各种因素影响,快,且卡在时间片结束前获得锁等等...,让某个线程能够一直获得锁,其他线程得不到)
//线程获得锁是竞争关系,不是排队关系


---------------------------------------------------------------------- 多线程,加锁,访问共享资源
int tickets = 10000;
pthread_mutex_t mutex;

void *threadRoutine(void *name)
{
string tname = static_cast<const char *>(name);

while (true)
{
// pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(2000); //抢票花费时间...
cout << tname << " get a ticket: " << tickets-- << endl;
// printf("%s get a ticket: %d\n",name,tickets--);
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
//可能出现的现象:某线程执行太快,加锁解锁特别频繁,其他线程阻塞状态不能立即获得资源 ---> 导致只有一个线程工作
//原因:单核,性能不足,多核性能不足等
//解决:让线程慢一点点..
//usleep(1000); //假设处理后续业务还要花费一点时间
}
return nullptr;
}

int main()
{
pthread_mutex_init(&mutex,nullptr);
pthread_t t[4];
int n = sizeof(t) / sizeof(t[0]);

for (int i = 0; i < n; i++)
{
char *tname = new char[64]; // C语言接口,传C字符串
snprintf(tname, 64, "thread-%d", i + 1);
pthread_create(t + i, nullptr, threadRoutine, tname);
}

for (int i = 0; i < n; i++)
{
pthread_join(t[i], nullptr);
}
pthread_mutex_destroy(&mutex);

return 0;
}
---------------------------------------------------------------------- 多线程,加锁,访问共享资源___End

 

锁本身就是个共享资源,锁要保护别人,也要保护自己,锁如何保护自己?

锁在加锁和解锁之间,可不可以被多线程切走?

1.凡是访问同一个临界资源(受保护的公共资源)的线程,都要先加锁,并且是必须是同一把锁,
2.每个线程访问临界区之前,都得先加锁,加锁本质是给 临界区(访问临界资源的代码) 加锁.
3.加锁粒度尽可能细
加锁一般建议是给最小集加锁,范围越小,越高效. 因为加锁是让线程访问资源串行化,串行化会降低执行效率.
4.加锁时,所有线程都必须要看到同一把锁. ---> 锁本身就是公共资源 ---> 锁如何保证自己的安全?
---> 加锁和解锁操作已被设计成原子的,所以无需担心 --->怎么设计的? ...
5.访问临界区过程中,线程有可能被切换,被调度.但被切换后,其他线程依旧无法访问临界区,依旧会阻塞在锁外 --- 安全,无影响
6.linux中锁被称为互斥量.


互斥量的原理和实现:
[[[[
.为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,
由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
//大部分汇编都是原子的
//汇编swap和exchange作用就如:原子实现两数据如int a,int b交换

汇编伪代码:
lock:
movb $0, %al //向寄存器al写入0 --- al寄存器是线程自己的上下文
xchgb %al, mutex //交换mutex和al寄存器的内容 --- 原子 //把mutex内的数据拿走就是加锁过程,拿到的是1就是解锁
if(al寄存器的内容>0) return 0; //返回,可以取得锁
else 挂起等待 //挂起
goto lock;

unlock:
movb $1, mutex //向mutex写入1 -- 直接改,不需要交换, // 向mutex写1就是加锁
唤醒等待mutex的进程
return 0;
解锁:
1.只要mutex为1就相当于释放锁了. 因为阻塞的线程下次能够从mutex获得1
2.解锁并不需要归还锁 --> 别的线程也可以释放锁

$加锁实际就是把mutex和线程自己的私有上下文做交换,0和1换来换去,没有增1和减1,保持一个1在流转
访问临界区的线程都在尝试从mutex获得1,谁交换成功谁就能访问
]]]]

 

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

$$$$ 线程安全 --- 主要就是关心数据的安全问题 -- 同时读写对数据造成的破坏

1.线程安全问题可能是调用了不可重入函数产生的,
也可能是自身安全问题,如除零,野指针,
也可能是自己使用了不同的函数访问了全局变量 -- 嵌套调用
...

###常见的线程安全的情况 --- 所有的安全情况尽可能掌握
.每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
.类或者接口对于线程来说都是原子操作
.多个线程之间的切换不会导致该接口的执行结果存在二义性

线程不安全一定是不好的.
//不可重入函数不是坏的,只要在使用时小心些,大部分函数都是不可重入函数


$$$$ 死锁

多线程代码 -> 并发访问临界资源 -> 加锁 -> "可能"导致死锁 -> 解决死锁问题

$$ 产生死锁的必要条件 --- 只要产生死锁,必定会有以下四个条件
1.互斥 --- 一次一人,独立使用,不共享
2.请求和保持 --- 申请资源阻塞 , 拥有临界资源,且请求临界资源
3.不剥夺条件 -- 没有特权解锁:一个线程不能解另一个线程的锁,只能由自己释放锁,不允许被别人释放锁
4.环路等待 --- 请求锁的关系链成环

//必要条件:只要产生了死锁,则必须发生的条件 --- 可能还有其他条件,但必要条件必须发生

$$ 如果避免死锁?
核心思想:破坏死锁的4个必要条件的其中一个

4个解决方法:
1.不加锁 --- 不是所有都要加锁,能不加锁就不加锁
2.主动释放锁 --- 使用如trylock这样的接口申请锁

>接口:trylock --- 尝试连续申请锁,如果多次申请锁不成功,则不再申请,并且释放掉已经获取到的锁
>声明:in t pthread_mutex_trylock(pthread_mutex_t *mutex);

3.控制线程统一释放锁 --- 如果在一段时间里线程没有动作,则同意释放
// 一个线程能被另一个线程释放锁. //加锁和解锁的汇编代码实现允许可以这么做
4.设置合理的申请锁顺序,避免成环


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

 

 

 

 

 

 

 

 


$$$$$$$$$ 线程同步
解决:为了合理的解决饥饿问题,在安全的规则下,多线程访问资源具有 一定 的顺序性
--- 线程同步 --- 让多线程进行协同工作

$ 饥饿问题:满足规则但不合理的问题
如:一个线程长期高优先级占有锁,使用锁,释放锁,导致其他线程低频或不能使用临界资源


// 有些调度器会优先调度特定进程,特定线程对锁的竞争能力更强,会导致出现同一个线程频繁调度锁,其他线程无法申请锁 的情况 -- 也属于同步问题

 

件变量是linux实现线程同步的一种方式,一个线程通过条件变量的方式通知另一个线程
$$ 条件变量:condition -- cond


###条件变量 -- 允许一个线程等待,让另一个线程去唤醒
.当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前(在某个不满足条件的状态下),它什么也做不了。
//意思是,在临界资源内一定会有条件判断,如果条件没有改变时,如果条件不满足,就如抢票没票时,抢票工作为循环加锁后又释放锁,什么都没做.并且优先级高,一直抢占资源 -- 空转
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

解决:如果条件变量不满足,应当让这个线程释放锁,并且进入休眠/阻塞.当条件满足时,再唤醒

 

 

$ 条件变量在phread库中是一个类型,维护了一个队列:当资源条件不满足时,让线程排队的队列

类型-定义:pthread_cond_t cond
pthread_cont_t cond = PTHREAD_COND_INITIALIZER; //全局条件变量初始化

接口:pthread_cond_init -- 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL

接口:pthread_cond_destroy -- 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond)

接口:pthread_cond_wait -- 释放当前线程的锁,并把线程放入等待队列中;当被唤醒后则会重新申请锁,申请到后会继续向后运行
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:cond:要在这个条件变量上等待
mutex:互斥量 --- 需要配合锁使用:会将该锁释放 --- 因为条件变量本身就是在临界资源内判断,必须先释放锁后才能去休眠,否则锁没机会被释放
//为了防止误唤醒(如同时唤醒多个,但在临界区内,不再执行判断语句时,可能会越过条件)
,需要将条件语句改成循环/轮询式,保证每次唤醒后,都是轮询式判断条件满足才允许向后运行,否则要再次回到等待队列.


$ 任何临界资源判断,操作,都必须在加锁和解锁之间.因为判断也是属于访问临界资源,只要是访问临界资源的操作都要遵守.
1. 即对临界资源的操作的代码要写在锁之间 ,持有锁的状态下访问临界资源
2. 要让线程进行休眠等待,不能拿着锁去休眠,必须要释放锁,否则其他线程也会阻塞,
---> 所以pthread_cond_wait有释放锁的能力

 


唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); -- 唤醒所有线程 -- 广播
int pthread_cond_signal(pthread_cond_t *cond); --- 唤醒一个线程

$ 唤醒后是从哪里开始执行? 在切走的地方继续运行!
--- 线程在哪里被切走就从哪里继续 -- 实现没有改变,还是使用线程切换那套
但是,返回后还是在pthread_cond_wait中,由于是在临界区,线程需要重新申请锁,申请成功后才能离开pthread_cond_wait,才能执行之后的任务
--- 所以线程被唤醒后,依旧是持有锁的状态去执行之后的任务.


---------------------------------------------------------------------- 条件变量初识
const int num = 5;

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* active(void*args)
{
const std::string name = static_cast<const char*>(args);

while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);//直接进入休眠
std::cout<<name<<" 活动"<<std::endl;
pthread_mutex_unlock(&mutex);
}

}

int main()
{
pthread_t tids[num];
for(int i = 0 ; i<num;i++)
{
char *name = new char[32];
snprintf(name,32,"thread-%d",i+1);
pthread_create(tids+i,nullptr,active,(void*)name);
}
sleep(1);
while(true)
{
std::cout<<"main wakeup thread ..." <<std::endl;
// pthread_cond_signal(&cond); //逐一唤醒
pthread_cond_broadcast(&cond); //全部唤醒
sleep(1);
}

for(auto& tid: tids)
{
pthread_join(tid,nullptr);
}

return 0;
}

1.在主线程中使用cond_signal逐个唤醒进程
观察发现,线程以一定顺序进行运行.

2.使用cond_broadcast一次全部唤醒
线程也是按一定顺序排队运行

---------------------------------------------------------------------- 条件变量初识___End

条件变量,允许多线程在cond中队列式等待(队列式等待就是一种顺序)

 


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

//西班牙语 --- 生产者(Productor)将产品交给店员(Clerk),消费者(Customer)

$$$ 生产者 - 消费者 模型 --- 允许生产和消费的步调可以不一致 --->高效


消费者 <---------------------> 超市 <--------------------> 供货商
交易场所 生产者

消费零散, 缓存 生产力高
允许生产和消费的步调可以不一致
效率高,忙闲不均

OS中: 线程 一种特定的缓冲区 线程
可以是各种数据结构

 

CP模型 producer-consumer 生产者-消费者模型
"321"原则
{
$ 3种关系 --- 通过锁和条件变量来维护
>1.生产者与生产者: 竞争 --- 竞争超市的空间 ----> 互斥
>2.生产者和消费者: 互斥或同步
a.同步/协调
b.竞争 --- 买和卖只能同时有1个,不是必须,但至少要有先后 --- 互斥
>3.消费者与消费者:互斥 -- 争夺资源

$ 两种角色:生产者,消费者 -- 两个线程

$ 一个交易场所(公共资源) -- 缓冲区
}

$ 手写CP问题,本质就是用代码维护实现"321"原则

 

 

基于BlockingQueue(阻塞队列)的生产者消费者模型(类似管道)

情况
1.队列为空时,消费者不能读
2.队列为满时,生产者不能写

消费者有消费者的条件变量,生产者有生产者的条件变量,都要是独立的,方便唤醒

同步:
1.一旦阻塞队列满了,那生产者就不再继续生产 ---> 到自己的队列中休眠
2.一旦阻塞队列空了,那消费者就不在继续消费 ---> 到自己的队列中休眠


维护同步的信号:
最小集:
1.生产者最清楚有没有数据:生产者push到交易场所后说明一定有数据 --> 给消费者发送有数据信号,唤醒消费者
2.消费者最清楚有没有空间:消费者从交易场所pop后说明一定有剩余空间 --> 唤醒生产者
还可以加入其他策略,1/3再唤醒,1/2再唤醒....

3.并且要维护同一类型线程的竞争关系,避免一个线程一直竞争使用锁,导致其他线程饥饿的现象出现 -- 需要使用条件变量,让不满足的线程进入休眠


阻塞队列要多大? --- 按任务需求'


高效在哪里? --
对生产消费模型的全面认识
1.数据从哪里来?
2.消费者拿到数据后,还需要对数据做加工处理

1.异步:在消费者对数据做加工处理的同时,生产者可以同时进行生产数据、获取数据,并且可以将数据缓存起来,让消费者直接获取,不影响消费者进行消费
也是一种解耦的表现

2.不仅可以生产整型,字符串字面值等,还可以生产对象(结构化数据),还可以生产任务(函数),让消费者去执行

阻塞队列 将公共资源视为一个整体,为1 ---> 使用1把锁

 

 

 

 

$$$$$ POSIX版本的 信号量(信号灯)
--- 本质是一个计数器:用于描述临界资源中资源的数目 semaphore

适用场景:临界资源被切成多块,优先使用信号量 //整体使用时优先使用互斥锁,条件变量

SystemV版本使用成本较高,POSIX适用于多线程和多进程中资源访问的情况

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于
线程间同步

信号量需要PV操作
P相当于 -- 操作
V相当于 ++ 操作
--- 这里的 '++' '--' 一定是原子的
即,信号量是一个原子性的计数器

当sem中资源数为1个时,该信号量称为二元信号量 --- 就是互斥锁

以前:单纯访问临界资源整体. 但如果多线程访问临界资源(数组)中内部小资源(元素). --- 如何让多线程并发的访问打资源中的不同的小资源?
--- 多元信号量

每一个线程,在访问对应的资源的时候,先申请信号量,申请成功则表示允许线程使用该资源.申请不成功则表示目前无法使用
//绝对不可能出现申请成功后没有资源的情况

信号量的工作机制:信号量机制类似于我们看电影买票,是一种资源的预定机制!

 

信号量本身就是临界资源的计数器. 可以直接在临界资源外得到临界资源数量,而不用进到临界区内
1.申请信号量成功,本身就表明资源可用,就是预定了一份资源,让这份资源预留给自己,其他人无法使用! -- 先预定
//条件变量是,等需要使用时才判断有没有的,可能有可能没有,有就能使用,没有就需要等待 ,必须要访问临界区才直到有没有 -- 所以一定在临界区内
2.申请信号量失败,说明没有资源可用 --- 只要申请信号量成功,就不用再判断条件是否满足
---> 即把判断转化成信号量的申请行为

申请信号量是在访问临界资源之前 --> 把判断资源是否就绪放到了加锁之前或访问临界资源之前 ---> 解决了什么问题?

认识接口: -- 线程共享资源部分
{

一.初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem_t *sem:信号量/信号灯
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值 -- 无符号类型 --- 设置信号量的个数


二.销毁信号量 int sem_destroy(sem_t *sem);

三.等待信号量
int sem_wait(sem_t *sem); //P()
功能:等待信号量,会将信号量的值减1 .相当于P操作

四.发布信号量
int sem_post(sem_t *sem);//V()
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

}


$$ 基于环形队列的生产消费模型

基于固定大小为N的环形队列 ---- 数组

1.index = i
i = i %= N;

2.队列,有头有尾
tail == head 时,队头 -- 可能是空也可能是满
tail = head->prev 时,队尾
tail用于指向数据为空的位置

由于实现环形队列对 (判空判满的问题) 不好控制,所以会有几种解决方案
1.队头和对位之间留一个位置
2.使用计数器控制

而信号量本身就是计数器,所以使用技术器方式控制判空判满的问题

>生产:生产者向tail中push数据
>消费:消费者向head中pop数据

生产者和消费者关心的"资源",是一样的吗? ------ 不一样
1.生产者关心的是资源的数量
2.消费者关心的是数据

环形队列只要我们访问不同的区域,生产和消费的行为可以同时进行吗? -- 可以

他们什么时候会访问同一个区域? 什么时候不会? (他们指生产者和消费者)
1.只有tail和head指向同一个区域时,他们才可能会同时访问同一个区域 --- 空的时候和满的时候
2.tail和head不等的时候,他们指向不同的区域

设一个时钟样的桌子,每个钟点间都有一个盘子,假设空间是盘子,资源是苹果 -- 消费者会取苹果,追逐生产者,生产者放苹果
规定:
1.一个盘子只能放1个苹果,生产者最多只能放12个苹果,不能超过消费者
2.消费者只能跟在生产者后面取苹果,不能超过生产者
他们什么时候会在同一个钟点区? 1.最开始的时候. 2.桌子上放满苹果的时候

当他们都在同一个位置时,就会存在竞争关系 -- 体现同步关系
3.当桌子为空的时候,只能让生产者往前走
4.当桌子为满的时候,只能让消费者往前走

环形队列约定
1.只有为空和为满时,c和p才会指向同一个位置
2.其他情况可以并发
3.当队列为空的时候,只能让生产者先走 -- head不能超过尾
4.当队列为满的时候,只能让消费者先走 -- tail不能超过头

 

生产者: | 消费者:
sem_room(空间数):N | sem_data(资源数):0
P(sem_room); -- 申请空间信号量:--sem_room |
//进行生产活动... |
V(sem_data); -- 生产了一个资源,释放一个资源信号 ---------+-----------> sem_data+1==>sem_data:1
| P(sem_data);//申请资源信号量:--sem_data
| //进行消费活动...
++sem_root; <------------------------------------------+----------- V(sem_root);
|
//... |
| // ...
|
|

//生产是生产一个资源,并占用一个资源.所以P(room),V(data)
//消费是消费一个资源,并归还一个资源.所以P(data),V(room)


读写可以并发,读读不行,写写不行

 

 

 


锁(与条件变量)与信号量 小结
1.什么时候用锁,什么时候用信号量?
a.资源看作整体时,使用锁.
b.资源被划分成多份并发访问时,使用信号量

 

 


$$$ 线程池

1.认识池化技术
策略:所有的池化技术,都是空间换时间的策略
原理:
1.在一次申请资源时,会在所需要的资源大小的基础上再额外申请一批资源.用于预防在下次申请的时候,能够更快的获取资源.减少申请所需要的开销.
(类似局部性原理).
2.如果预测到一种资源是未来会很频繁,很急需的资源,那在申请前预先准备好管理一批资源,之后则在申请时能够大幅减少申请的开销.

2.线程池:
对应模型:生产者-消费者模型
消费者线程 - 生产者线程

/*threadpool.h*/
/* 线程池:
* 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。
而线程池维护着多个线程,等待着 监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。
线程池不仅能够保证内核的充分利 用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
* 线程池的应用场景:
* 1. 需要大量的线程来完成任务,且完成任务的时间比较短。
WEB服务器完成网页请求这样的任务,使用线程池技 术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。
但对于长时间的任务,比如一个 Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
* 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
* 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
突发性大量客户请求,在没有线程池情 况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,
短时间内产生大量线程可能使内存到达极限, 出现错误.
* 线程池的种类:
* 线程池示例:
* 1. 创建固定数量线程池,循环从任务队列中获取任务对象,
* 2. 获取到任务对象后,执行任务对象中的任务接口
*/

 

 

$$$ 单例模式
//饿汉模式

$ 懒汉模式

思想:延迟加载
运用了延迟加载思想或类似延迟思想的还有: 写时拷贝,动态库,缓存/局部性原理,虚拟地址空间

 

 

 

$ 其他线程安全
STL中的容器是否是线程安全的? 不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

智能指针是否是线程安全的?
1.对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
2.对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题,
基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.

 

 

 

$ 常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行
锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,
会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不
等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁,公平锁,非公平锁?

悲观锁:先加锁,后访问数据
乐观锁:
CAS操作:C++少,JAVA多
自旋锁:
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
使用:由程序员控制,根据临界区的时间,决定使用自旋还是阻塞. 使用方式基本和mutex类似

 

 

 


$$ 读者-写者问题

一样有"321"原则:
3种关系:
1.读读(没关系,随便读)
因为数据是可以重复利用的,仅仅读数据(拷贝),不会消耗数据; 而cp问题中,消费者会把数据拿走
2.写写(互斥),
3.读写(互斥,同步)
同步: 写满不能再写,必须要读.空了不能读,要等写
2种角色:
读者-写着
1个场所:


接口:
/*

读写锁:pthread_rwlock_t rwlock

int pthread_rwlock_destroy( pthread_rwlock_t *rwlock );
int pthread_rwlock_init ( pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
( &rwlock , nullptr )l;

//读者的加锁方式
int pthread_rwlock_rdlock ( pthread_rwlock_t *rwlock ); //rdlock = read lock;
int pthread_rwlock_tryrdlock( pthread_rwlock_t *rwlock );

//写者的加锁方式
int pthread_rwlock_trywrlock ( pthread_rwlock_t *rwlock );
int pthread_rwlock_wrlock ( pthread_rwlock_t *rwlock ); //wr = write

//读者写者统一的释放方式
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);


//设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

/*pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG, 导致表现行为和 PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁 */

*/


伪代码:
/*

*/

实际应用举例:
如:博客发表出去后,基本都是让别人读,很少修改(读者优先)
/*

*/

读写锁两种策略:
1.读者优先策略(常规,默认,写者可能饥饿),写者饥饿是客观存在的,可以被允许的
2.写者优先策略
控制读者进入,不影响正在读的读者,阻塞后面将要读的读者

 

标签:--,pthread,汇总,---,int,60000,线程,linux,进程
From: https://www.cnblogs.com/DSCL-ing/p/18038550

相关文章

  • linux网络编程基础知识汇总(更新中)
    阿帕网arpanet阿帕网为美国国防部高级研究计划署开发的世界上第一个运营的封包交换网络,它是全球互联网的始祖。局域网LAN(LocalAreaNetwork):通过路由器和交换机把计算机连接在一起广域网WAN(WideAreaNetwork)//广域网和局域网没有明显的界限,是一个相对的概念,一般把......
  • Linux使用命令行编译并用st-link烧录STM32
    创建工程在STM32CubeMX中配置,选择Makefile并生成。环境安装编译工程需要用到arm-none-eabi,去官网下载对应系统版本,下载后解压到任意位置。添加环境变量添加环境变量到.bashrc文件:echo'exportPATH="/toolchain/arm-none-eabi/bin:$PATH"'>>~/.bashrc我解压的位置为/too......
  • linux基本知识汇总1(基础命令) 20000字汇总
    linux版本号主版本号.次版本号.修正次数2.6.30--次版本号为偶数:稳定版奇数:测试版$$$$命令选项查看方式1.内建命令(help)格式:help+内建命令####help命令//命令使用说明2.外部命令(–help)一般是Linux命令自带的帮助信息,并不是所有命令都自带这个......
  • Windows下写脚本无法运行在linux上?怎麽办?
    Windows下写脚本无法运行在linux上?怎麽办?$‘\r‘:commandnotfound的解决方法在Linux系统中,运行Shell脚本,出现了如下错误:one-more.sh:line1:$'\r':commandnotfound1出现这样的错误,是因为Shell脚本在Windows系统编写时,每行结尾是\r\n,而在Linux系统中行每行结尾是\n,......
  • Linux 命令指南
    做这个东西有两个用处,一是初赛会考,二是考场上用windows哪里数组越界你都不知道直接RE爆炸。sudo-s输入后填写密码获得管理员权限。cd打开文件或者目录,用法是cd目录名。cd/退回到根目录。mkdir创建一个目录,使用方法为mkdir目录名。ls显示当前目录下的......
  • 安装虚拟机(Linux)
    安装虚拟机的过程:1.创建虚拟机2.不用动,点击下一步3.选择稍后安装操作系统,然后继续点击下一步4.选择Linux,版本选择CentOS7645.设置虚拟机的名称以及位置6.设置磁盘大小7.点击完成8.配置虚拟机9.设置内存10.设置处理器11.配置ISO映像文件12.打开虚拟机1......
  • linux虚拟机安装
    1.点击文件——新建虚拟机或创建新的虚拟机2.选择典型点击下一步3.选择稍后安装操作系统,点击下一步4.客户机操作系统选择Linux(L),版本选择CentOS764位5.可随意更改名称,创建文件夹6.设置虚拟机可用内存7.虚拟机创建完成点击完成即可......
  • Linux 性能调优之虚拟化调优
    Linux性能调优之虚拟化调优https://mp.weixin.qq.com/s/ypAr1qvAYBFD2BKyjx3HBg写在前面考试整理相关笔记博文内容涉及LinuxVM常见管理操作以及部分调优配置理解不足小伙伴帮忙指正不必太纠结于当下,也不必太忧虑未来,当你经历过一些事情的时候,眼前的风景已经和从前不一......
  • linux shell 中实现进度条
     linux shell中实现进度条:#!/bin/bashtotal_steps=100for((step=1;step<=total_steps;step++));doprintf"\r[%-50s]%d%%"$(printf"#%.0s"$(seq1$((step*50/total_steps))))$((step*100/total_steps))sleep0.1#模拟操作延迟d......
  • 转:Linux文件权限详解
    Linux文件权限详解_linux文件权限-CSDN博客掌握Linux文件权限,看这篇就够了-知乎(zhihu.com)  ......