系统编程学习笔记

本文是本人本科时学习系统编程的笔记。

1.进程概念

系统编程阶段:
进程概念 进程控制 基础IO 进程间通信 进程信号 多线程

冯诺依曼体系:计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。运算器+控制器=CPU,既运算,也控制
cpu处理速度快,且没地方存放多余数据,输出设备慢,cpu处理完了交给存储器,输出设备慢慢从存储器拿数据
存储器是中间的缓冲。cpu控制输入输出存储。

硬件结构决定软件行为:数据流向

操作系统:软件
目的:让计算机更好用
功能:统筹管理计算机上的软硬件资源

操作系统描述各硬件是结构体

库函数就是对系统调用接口的一层封装,上下级调用关系。
操作系统:对下管理软硬件资源,对上提供良好的执行环境
管理:先描述,再组织进行管理

进程概念
进程:
对于用户角度进程就是运行中的程序
站在操作系统角度进程就是运行中的一个描述,就是PCB
在Linux下进程就是一个结构体,叫struct task_struct{}

程序:一堆代码–在硬盘内,程序运行起来就会被加载到内存中(被删除还会继续运行)
操作系统管理进程:描述进程=PCB(process control block)进程控制块
操作系统通过PCB来管理进行中的程序
PCB包含内容:进程标识符(PID,process ID),进程状态,优先级,程序计数器(存储即将执行的 指令),上下文数据(正在处理的数据),记账信息(进程运行了多久),内存指针(程序数据存在内存里的位置),IO状态信息
进程就是PCB,PCB就是进程.

进程查看:
/proc 进程运行信息存放目录
ps -aux查看系统上的进程信息
ps -ef 查看系统上的简略进程信息
tty 显示所在终端编号
getpid() 函数作用:获取调用进程的进程ID

进程创建:
fork()----#include <unistd.h>
通过复制调用进程,创建一个新进程(子进程)
子进程复制的就是父进程的PCB(父子进程数据,代码看起来都一样)
—>代码共享,数据独有
子进程并非从头开始运行,而是父进程复制出子进程前那一刻

结果打印了两次。

返回值:
对于父进程来说,fork返回值是子进程的pid;创建子进程失败返回-1;
对于子进程来说,fork返回值是0;
因为父子进程代码运行一样,所以需要通过返回值来分流父子进程

创建子进程的意义:完成其他任务,分摊运行压力
如何让子进程完成其他任务:首先分辨出父与子进程,然后根据返回值进行分流

例题1:fork fork fork。
例题2:for(int i=0,i<2,i++){
fork();
printf(“-”);
}
问打印几次-8次。第二问printf -\n会打印几次-6次

进程状态:就绪,运行,阻塞
Linux下进程状态:运行R,可中断睡眠S,不可中断睡眠D,停止T,死亡X,僵死Z
kill PID 消灭进程
停止状态是杀不死的.kill -18 PID.唤醒再杀.
kill -9 PID 强杀
kill杀不死的成为僵尸进程(僵死状态下的进程)

产生原因:子进程先于父进程退出,操作系统检测到进程退出,通知父进程,而父进程在打麻将,没关注这个通知,这时操作系统为了封锁现场,不会释放子进程资源,因为子进程的PCB中包含死亡(退出)原因,这时候因为既没有运行,也没有完全退出,因此处于僵死状态,成为僵尸进程。
危害:资源泄露
处理:关闭父进程
处理办法:极力避免,进程等待

孤儿进程:父进程先于子进程退出,父进程退出后,子进程成为后台进程,并且父进程成为1号进程。
守护进程:特殊(脱离与终端+会话的关联)的孤儿进程

进程优先级:
优先级:决定资源优先分配权的等级划分
为什么要有优先级:让操作系统运行更合理
交互式进程:一旦有操作要优先处理
批处理进程:一直处理数据,但对cpu要求不高
设置:ps -efl
PRI 优先级 NI nice值: PRI无法直接设置,但是可以通过设置NI值,进而调整PRI值
PRI=PRI+NI
renice -n size -p pid
nice -n size ./main
nice值范围:-20~19
IO密集型程序,CPU密集型程序

并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰

环境变量:保持系统运行环境参数的变量 --全局特性
echo 通过变量名称查看指定环境变量
env 查看所有环境变量
set 查看环境变量以及临时变量
export 声明/设置一个环境变量
unset 删除一个变量
常见环境变量:HOME SHELL PATH
代码中的操作以及特性:int main(,charenv[]) extern char**environ;chargetenv(char*)

程序地址空间:虚拟地址空间—内存描述符mm_struct
作用:保持进程独立性,通过页表物理地址–充分利用物理内存,增加内存访问控制
电脑使用分页式内存管理,每4K一页。
页号,页内偏移(在哪页,偏移量多少),页表,管理虚拟地址和物理地址

2.进程控制

进程创建:fork,vfork,clone
进程终止:进程退出场景:1,正常退出,结果符合预期 2,正常退出,结果不符合预期 3,异常退出
退出方式return exit_exit; $? ; errno --perror strerror;
进程等待:等待子进程状态改变(等待子进程终止)—避免产生僵尸进程
pid_wait(int statu)阻塞等待任意一个子进程退出,没有子进程退出在一直等待,获取子进程退出返回值,返回退出子进程的pid
waitpid(int pid,int*statu,int opt)
默认阻塞等待任意一个子进程/指定子进程退出,没有子进程退出则一直等待
但是waitpid第三个参数可以将waitpid设置为非阻塞,没有子进程退出则立即报错返回0
阻塞/非阻塞:完成功能的时候如果不具备完成条件,区别就是是否立即返回
程序替换:将进程的虚拟地址空间所映射在物理内存的区域进行改变,改编成另一个程序在内存中的位置,更新页表信息,重新初始化虚拟地址空间。
为了让进程运行另一个程序,常用于
操作系统没有给我们创建父进程(PCB)的接口,只能通过创建子进程的方式来创建新进程去完成某事。子进程既可以用来分摊压力,也可以做某件事,就没必要给接口去创建父进程。

execl execlp execle
execv execvp execve
l和v区别:程序运行参数的赋予方式不同 execl(/bin/ls,ls,-a。。。)
有无p的区别:程序名称是否需要带路径execl()
有无e的区别:是否自定义环境变量 execl(/bin/ls,…) execl(/bin/ls,…,char*env[])

自己实现minishell,体会shell原理以及程序替换的目的:
shell处理流程:
while(1){
1 获取标准输入
2 对输入字符串进行解析
(获取程序名称+参数)
3 创建子进程
1 程序替换-程序名称
4 进程等待
}

3.基础 IO

回顾标准库IO接口:fopen fclose fwrite fread fseek fgets fprint sscanf
fopen :
r :只读方式打开文件
r+ :读写方式打开文件
w :只写方式打开文件,文件不存在则创建,存在则清空内容
w+ :读写方式打开文件,文件不存在则创建,存在则清空内容
a :追加方式打开文件,文件不存在则创建,每次写入数据都是写入文件末尾
a+
stdout stdin stderr FILE*

系统调用IO接口学习:open(O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC O_APPEND)
write read close lseek

文件流指针和文件描述符关系:文件流指针结构中包含了文件描述符的成员变量
并且我们所说的缓冲区 fflush(stdout)—也是文件流指针中维护的缓冲区

标准输入 标准输出 标准错误
stdout stdin stderr
宏0 1 2

文件描述符是什么?
系统调用接口open打开文件返回的正整数。
进程打开文件后,使用file结构体描述文件,使用file*fd_array[]组织描述信息;然后将打开的文件在数组中的下标返回给用户,作为句柄进行操作
fd_array这个数组在file_struct这个结构体中,file_strct结构体在pcb中

文件描述符最小分配原则:
重定向:针对文件描述符的重定向----改变文件描述符这个下标所对应的文件描述信息
dup2(int oldfd,int newfd) fd=open(file) dup2(fd,1)
minishell中实现重定向:获取字符串-》解析是否重定向(> >>+文件名)

文件系统

文件存储流程:通过inode_bitmap在inode table中找到空闲的inode节点,通过data_bitmap在数据块区域找到空闲数据块,将数据位置信息,记录到inode节点中,将文件数据写入数据块中;将文件名和inode节点号写入父目录文件中
目录文件中:存放了一张目录下有什么文件的表,表中记录了(文件名,inode节点号)–目录项

cat./a.txt流程:在当前目录文件中查找文件名信息,通过文件名获取inode节点号,通过inode节点号,找到inode节点,进而访问数据块,读取数据进行打印。

软链接/硬链接
创建硬链接文件:ln a.txt a.hard
创建软链接文件:ln -s a.txt a.soft
软链接文件是一个独立文件,像是一个源文件的快捷方式文件–inode节点号不同
硬链接文件是一个文件的另一个名字,跟源文件并没有什么区别—inode节点号相同

删除源文件,软链接失效(通过文件名查找源文件)
删除源文件,硬链接无影响(通过inode节点找文件只是链接数-1)
软链接文件可以跨分区建立,硬链接不可以
软链接文件可以针对目录进行创建;硬链接不可以

静态库/动态库的生成与使用

4.信号

信号的注册:信号在进程中的注册

不可靠信号注册:判断pending信号集合位图相应位是否为1;若为0,为信号组织sigqueue节点添加到链表中,并且pending位图置1;
若为1(信号已经注册过还没有被处理),则什么都不做(等于丢弃)
可靠信号注册:不管位图是否为一,阻止节点,添加到链表中,并且位图置一(信号不会被丢弃)

信号的注销:
非可靠信号:因为非可靠信号的信号节点只有一个,因此删除节点,位图直接置0
可靠信号:因为可靠信号的信号节点有可能会有多个,若还有相同信号节点,则位图依然置1,否则置0

信号的处理:

默认 SIG_DFL signal(int signum,sighandler_t handle)
忽略 SIG_IGN 修改信号的处理方式
自定义

信号自定义处理方式的捕捉流程:

信号的阻塞:暂时阻止信号被抵达-----信号依然可以注册,但是只是暂时不处理,解除阻塞之后才会处理

抵达:动作—信号的处理
未决:状态—信号冲产生到处理之前所处的状态
信号的阻塞过程实际就是,在pcb的blocken信号阻塞集合中标记哪些信号,到来之后暂时不处理(将blocked位图集合中对应的位 置1,表示阻塞这个信号)

nerd tree,vim插件

有两个信号SIGKILL-9 SIGSTOP-19无法被阻塞,无法自定义,无法被忽略
接口:
sigprocmask 阻塞/解除阻塞信号
sigemptyset 清空信号集合
sigfillset 向集合中添加所有信号
sigaddset 向集合中添加制定信号
sigismember 判断信号是否在集合中
sigdelset 从集合中移除执行信号
sigpending 获取未决信号

静态条件

函数中所完成的操作并非原子性操作—并且操作的数据是一个全局数据
如果一个函数中操作了全局性数据,并且这个操作不是原子性操作,并且这个操作不受保护,则这个函数是一个不可重入函数
不可重入函数:不能再多个时序中重复调用(重复调用有可能会造成数据二义性)
可重入函数:在多个时序的运行中重复调用,不会造成异常影响(数据二义性问腿)
不可重入函数举例:malloc/free

5.多线程

【大纲】
*
线程概念
*
线程控制:线程创建 线程终止 线程等待 线程分离
*
线程安全
*
同步与互斥:互斥锁,条件变量,(生产者与消费者模型)posix标准信号量,读写锁(读写者模型)。
*
线程池:理解与基本实现
*
设计模式:线程安全的单例模式

线程概念: 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
一切进程至少都有一个执行线程 线程在进程内部运行,本质是在进程地址空间内运行

linux下pcb是线程,因为linux下线程以进程pcb模拟实现线程。linux线程也叫轻量级进程。进程就是线程组。

线程是cpu调度的基本单位(linux下),因为进程是线程组—程序运行起来,资源是分配给整个线程组的,因此进程是资源分配的基本单位

多进程可以并发任务/多线程也可以并发多任务 用哪个好? 优缺点对比:
*
一个进程中的线程共用同一个虚拟地址空间
*
线程间通信更加方便
*
线程的创建/销毁成本更低
*
线程间切换调度成本更低
*
线程的执行粒度更细
*
线程之间缺乏访问控制–系统调用(exit),异常针对的是整个进程,健壮性低
*
shell怕自己崩所以用多进程,子进程怎么出问题都不怕,多进程更加稳定。)

多进程/多线程进行多任务处理的共同优点:都可以并发并行处理任务,提高处理效率
cpu密集型程序:程序都是大量的运算操作
io密集型程序:程序中都是大量的io操作
共同缺点:对临界资源操作需要考虑更多,编码更加复杂
多进程场景:对主进程安全度要求特别高的程序-----比如shell
vfork常见一个子进程共用同一个虚拟地址空间,怕出现调用栈混乱,因此子进程运行完毕或程序替换后父进程才开始运行。
多线程pcb使用同一个虚拟地址空间:如何实现同时运行而不会出现调用栈混乱
为每个线程在虚拟地址空间中单独分配一块空间

每个线程都会有一些独立的信息
*

*
寄存器
*
errno
*
信号屏蔽字
*
调度优先级

线程之间共享的数据:
*
代码段,数据段
*
文件描述符表
*
每种信号的处理方式
*
用户id,组id
*
当前工作路径

线程控制:线程创建,线程终止,线程等待,线程分离
fork vfork clone
操作系统并没有为用户提供直接创建线程的系统调用接口,因此大佬们自己封装了一套线程库,实现线程控制
因此有人称我创建的线程是一个用户态线程,在内核中对应了一个轻量级进程实现程序的调度运行

线程创建:
pthread_creat -lmytest
pthread.h 因为是库函数,因此编译链接是需要加上-pthread/-lpthread链接线程库。

ps-L 查看轻量级进程(线程)信息
tid task_struct-pid task_struct-tgid
线程地址空间首地址 LWP PID=主线程的pid

线程终止:在线程入口函数中return;
pthread_exit 退出调用线程
主线程退出,进程并不会退出
线程退出也会成为僵尸进程(但是普通线程提出不出效果)
线程地址空间无法被回收再利用,造成内存泄漏
pthread_cancel 取消一个指定线程

线程等待:获取制定线程的返回值,允许系统回收资源
一个线程创建起来,默认有一个属性,joinable
处于joinable属性的线程,退出后必须被等待,因为线程退出后,为了保存退出返回值,不会自动回收线程的资源(成为僵尸线程)

使用pthread_join接口实现线程等待,获取指定线程返回值,允许系统释放资源。

线程分离:将线程的一个属性从joinable设置为detach属性
处于detach属性的线程,退出后资源直接自动被回收,这类线程不能被等待。
用法:通常如果用户对线程的返回值并不关心,则在创建线程之后直接分离线程或者在线程入口函数中第一时间分离自己。
pthread_detach(pthread_t tid)

课后调研:一个线程被取消,返回值是多少

线程安全:多个线程同时操作临界资源而不会出现数据二义性
在线程中是否对临界资源进行了非原子操作
可重入/不可重入:多个执行流中是否可以同时进入函数运行而不会出现问题
如何实现线程安全:
同步:临界资源的合理访问
互斥:临界资源同一时间唯一访问
互斥如何实现:
互斥锁:如何实现安全操作? 1/0
一个0/1的计数器–
1表示可以加锁,加锁就是计数-1
操作完毕之后要解锁,解锁就是计数+1
0表示不可以加锁,不能加锁等待

互斥锁操作步骤:
1.
定义互斥变量 pthread_mutex_t
2.
初始化互斥锁变量 pthread_mutex_init
3.
加锁 pthread_mutex_lock
4.
解锁 pthread_mutex_unlock
5.
销毁互斥锁 pthread_mutex_destroy

死锁:因为对一些无法加锁的锁进行加锁而导致程序卡死

死锁产生的四个必要条件:
1.
互斥条件( )
2.
不可剥夺条件(我的锁, )
3.
请求与保持条件(拿着手里的,请求其他的,其他的请求不到,手里的也不收)
4.
环路等待条件

产生场景:加锁/解锁顺序不同
预防死锁:破坏必要条件
避免死锁:死锁检测算法,银行家算法–课后调研
死锁处理:

同步的实现:临界资源访问合理性-----生产出来才能使用
没有资源则等待(死等),生产资源后唤醒等待
条件变量:
1,定义条件变量
2,初始化条件变量
3,等待/唤醒

6.多线程

复习:
多线程
线程安全:线程间对临界资源进行竞争操作时若不会造成数据二义则时线程安全的;否则,则时线程不安全

如何实现线程安全:
同步:临界资源访问的时序可控
互斥:临界资源的唯一访问
如何实现互斥:互斥锁:原子操作的计数器
pthread_mutex_t pthread_mutex_init pthread_mutex_destroy
pthread_mutex_ lock pthread_mutex_trylock pthread_mutex_timedlock
pthread_mutex_unlock

死锁:
产生:对锁资源的竞争以及进程/线程加锁的推进顺序不当
产生的必要条件:互斥,不可剥夺条件,请求与保持条件,环路等待条件
预防:破坏必要条件
处理:银行家算法

如何实现同步:唤醒+等待
线程1如果操作条件满足则操作,否则等待
线程2促使条件满足,唤醒等待的线程
pthread_cond_wait pthread_cond_signal/_broadcast

条件变量为什么要搭配互斥锁使用?
因为条件变量本身只提供等待与唤醒的功能,具体什么时候等待需要用户来判断,这个条件的判断,通常涉及临界资源的操作(其他线程要通过修改条件,来促使条件满足),而这个临界资源的操作应该受保护,因此搭配互斥锁一起使用。
加锁
条件判断—不满足
解锁pthread_mutex_unlock
等待pthread_cond_wait ------1,解锁 2,休眠 3,被唤醒后加锁

多个吃面/做面的人:
因为促使条件满足后,pthread_cond_signal唤醒至少一个等待线程,导致因为条件的判断时一个if语句而造成一碗面多次吃的情况(第一个吃面的人加锁吃碗面之后解锁,第二个被唤醒的吃面人,等待再锁上刚好拿到锁,继续向下走-吃面),因为条件的判断需要使用while循环判断。

因为促使条件满足后,pthread_cond_wait唤醒的时所有等待在条件变量上的线程,但是有可能被唤醒的这个线程也是一个做面的线程,因为已经有面,条件不满足而陷入等待,导致死等。
本质原因:唤醒的时候,唤醒了错误的角色(因为不同的角色等待在同一个条件变量上)
因此,线程有多少种角色,就应该有多少个条件变量,分别等待;分别唤醒。

生产者与消费者模型:
如何保证生产者与消费者的线程安全?
1,生产者与生产者之间应该具有互斥关系
2,消费者与消费者之间应该具有互斥关系
3,生产者与消费者之间应该具有同步+互斥关系
手撕生产者与消费者模型:一个场所,两种角色,三种关系
场所:线程安全的队列
BlockQueue
std::queue<> QueuPush
int_capacity QueuePop
mutex
cond_productor
cond_consumer
功能:1 解耦合,2 支持忙闲不均,3 支持并发

信号量:计数器+等待队列+等待+唤醒
功能:实现线程/进程间的同步与互斥
计数器就是判断的条件—当计数只有0/1的时候那么就可以实现互斥了
等待队列+等待+唤醒实现同步的基本功能

system V信号量:信号量原语:P(-1+阻塞)/V(+1+唤醒)操作
posix信号量:
定义:sem_t 信号量变量
初始化:sem_init
数据操作前资源计数判断:sem_wait
计数>0则计数-1,直接返回,往下操作
计数<=0则计数-1,阻塞等待
生产数据后则计数+1,唤醒等待:sem_post
销毁:sem_destroy

RingQueue{
std::vector_queue(10)
int_write_step;
int_read_step;
sem_t_sem_data;//数据计数
sem_t_sem_idle;//空闲空间计数
sem_t_sem_lock;//锁
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值