系统编程
1.基础入门
1.1基本函数
open()函数:打开一个文件
int open(char *pathname , int flags , mode_t mode)
参数:
pathname:想要打开的文件路径名
flags(头文件#include
):文件打开方式:O_CREAT|OAPPEND|… mode:权限,当第二个参数设置了O_CREAT,就需要写该参数
mode:设置文件的权限(参数3使用前提:参数2指定了O_CREAT)
返回值:
成功:打开文件所得到对应的文件描述符(整数)
失败:-1,设置errno
read()函数:往一个文件读内容
ssize_t read(int fd , void *buf , size_t count)
参数:
fd:文件描述符
buf:存数据的缓冲区
count设置读字节数大小
count:缓冲区大小
返回值:
成功:读到的字节数
失败:-1,设置errno
write()函数
ssize_t write(int fd , const void *buf , size_t count)
参数:
fd:文件描述符
buf:待写出数据的缓冲区
count:写入数据字节数大小
返回值:
成功:写入的字节数
失败:-1,设置errno
1 | //将文件cp一份 |
1.2基本知识
1.PCB进程控制块:本质是结构体
- 成员:文件描述符表——>文件描述符:0/1/2/3/…/1023
key value(指针:指向一个结构体)
0 – STDIN_FILENO ——–>标准输入
1 – STDOUT_FILENO —–>标准输出
2 – STDERR_FILENO ——>标准错误
注意:每次得到的文件描述符默认是表中可用最小的
2.阻塞、非阻塞
是设备文件、网络文件的属性。产生阻塞的场景:读设备文件、读网络文件(读常规文件无阻塞概念)
dev/tty:终端文件。标准输入、标准输出、标准错误都在终端显示
非阻塞设置:open(“/dev/tty”,..|O_NONBLOCK)
- 此时若返回-1,并且errno = EAGIN或EWOULDBLOCK,说明不是read失败,而是read在以非阻塞方式读一个设备文件或网络文件,并且文件无数据(默认是阻塞状态)
1 | int main(void){ |
3.参数
传入参数:
指针作为函数参数
有const关键字修饰
指针指向有效区域,在函数内部做读操作
传出参数:
指针作为函数参数
在函数调用之前,指针指向的空间可以无意义,但必须有效
在函数内部,做写操作
函数调用结束后,充当函数返回值
传入传出参数:
指针作为函数参数
在函数调用之前,指针指向的空间有实际意义
在函数内部,先做读操作,后做写操作
函数调用结束后,充当函数返回值
4.文件存储
dentry:目录项,其本质是结构体,重要成员变量有两个{文件名,inode,…},而文件内容(data)保存在磁盘块中。
inode:其本质是结构体,存储文件的属性信息。如权限、类型、大小、盘块位置…。大多数的inode都存储在磁盘上,少数常用、近期使用的inode会被存储在内存上。
1.3进阶函数
1.fcntl函数
获取文件状态:F_GETFL
设置文件状态:F_SETFL
获取fd描述符的stdin属性信息:int flgs = fcntl(fd,F_GETFL);
添加非阻塞状态:flgs |= O_NONBLOCK;
把设置的状态信息设置到终端设备的状态信息中:fcntl(fd,F_SETFL,flgs);
1 |
|
2.lseek函数
off_t lseek(int fd , off_t offset , int whence);
参数:
fd: 文件描述符
offset:偏移量
whence:偏移位置:SEEK_SET(起点)/SEEK_CUR(当前位置)/SEEK_END(终点)
返回值:
成功:较起始位置偏移量
失败:-1 设置errno
应用场景:
文件的读和写使用同一偏移位置(读和写都改变偏移位置)
使用lseek获取文件大小
使用lseek拓展文件大小(要想使文件大小真正拓展,必须引起IO操作)
补:可以使用truncate函数,直接拓展文件大小
- 如int ret = truncate(“dict.txt”,250);
1 | //测试文件的读和写使用同一偏移位置 |
1 | //使用lseek获取文件大小 |
3.stat/lstat函数
stat底层是一个结构体,里面有文件的信息
int stat(const char *path , struct stat *buf);
参数:
path:文件或目录路径
buf:(传出参数)存放文件属性
返回值:
成功:0
失败:-1 设置errno
系统提供函数
获取文件大小:buf.st_size
获取文件类型:buf.st_mode
获取文件权限:buf.st_mode
符号穿透:stat会;lstat不会
1 | int main(){ |
4.link/unlink函数
link函数:可以为已经存在的文件创建目录项(硬链接)
1 | link("a.txt","b.txt"); //旧文件;新文件 |
unlink函数:删除一个文件的目录项。从某种意义上说,只是让文件具备了释放的条件。
unlink函数的特征:清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放(只是目录中我们看不到该文件了而已),要等到所有打开该文件的进程关闭该文件,系统才会挑时间将该文件释放掉
1 | int main(void){ |
5.readlink()函数
作用:读取符号链接(软链接)文件本身内容,得到链接所指向的文件名。
如有软链接:t.soft -> /home/hoem1/test
终端执行:readlink t.soft 得到:/home/hoem1/test
6.目录操作函数
DIR * opendir(char *name);
int closedir(DIR *dp)
struct dirent *readdir(DIR *dp);
其中:
1 | struct dirent{ |
1 | int main(int argc,char *argv[]){ |
案例:递归遍历目录
1 | void isFile(char *name); //申明isFile函数 |
7.dup/dup2函数
int dup(int oldfd);
参数:oldfd:已有文件描述符
返回值:新文件描述符
int dup2(int oldfd,int newfd);
参数:
oldfd:原文件描述符
newfd:新文件描述符—–>指向源文件描述符所指向的文件
返回值:新文件描述符
1 | int main(int argc,char *argv[]){ |
fcntl函数是实现dup
int fcntl(int fd,int cmd,…);
参数:
fd:原文件描述符
cmd:F_DUPFD(特定参数)
参数3:指定文件描述符号。被占用的,返回最小可用的;未被占用的,返回=该值的文件描述符
返回值:返回新的文件描述符
1 | //fcntl也可以像dup那些使用 |
2.进程
2.1进程与程序
程序:死的,只占用磁盘空间 —剧本
进程:活的,运行起来的程序,占用内存、cpu等系统资源 —戏剧
1.进程控制块PCB
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
内部成员:
进程id:系统中每个进程有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数;
进程的状态:有初试、就绪、运行、挂起和停止五种状态
进程切换时想要保存和恢复的一些CPU寄存器;(了解)
描述虚拟地址空间的信息;(了解)
描述控制终端的信息;(了解)
当前所处工作目录;
umask掩码:rwx对应124,当umask为022时,在你创建文件和目录时,默认权限是777-022=755,即rwxr_xr_x;(了解)
文件描述符表:是map结构,key为正整数,value为指针,指针指向结构体;它包含很多指向file结构体的指针(每个进程都有文件描述符表)
和信号相关的信息;
用户id和组id;
会话和进程组;(了解)
进程可用使用的资源上限;(了解)
2.进程共享
父进程在fork()之后:
相同:全局变量、data段、text段、栈、堆、环境变量、用户id、宿主目录、进程工作目录、信号处理方式…
不相同:进程id、fork返回值、父进程id、进程运行时间、闹钟(定时器)、未决信号集
注:对于全局变量,父子进程间遵循读时共享写时复制的原则
3.孤儿进程/僵尸进程
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程(回收)
僵尸进程:子进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程。(kill对其无效)
3.相关函数
3.1fork()函数
功能:是创建一个新的进程
pid_t fork(void);
- 返回值:对应文件的子进程id号
注意:创建出来的子进程可用执行父进程中fork()函数下面的代码。在父进程中,fork返回的是子进程id号;在子进程中,fork返回的是0;
补:getpid()返回的是当前进程;getppid()返回的是父进程;
1 | //通过fork()函数来生成子进程 |
在上面代码中,子进程和父进程都会执行sleep(3)这行代码,不是说只有让父进程执行,而子进程不执行,sleep(3)只是防止父进程结束退出程序了,子进程还没有打印父进程。如果父进程结束了,子进程才打印父进程,则父进程是1
1 | //循环创建5个子线程,且有序打印出 |
3.2 execlp()函数
功能:指定进程执行相应的程序
int execlp(const *file , const *arg , …);
参数:
file:要加载(执行)的程序名字
arg以及后面:调用该程序的命令
返回值:成功无返回(执行指定程序去了),失败返回-1
该函数常用来调用系统程序。如ls、date、cp、cat等命令
1 | //execlp():是让子线程不执行父线程的代码,而是执行/execlp所指定的代码或函数 |
3.3 execl函数
功能:既可以执行自己写的程序,也可用执行系统程序
int execl(const char* path , const char *arg , ….);
参数:
path:程序的路径
arg以及后面:执行该程序的命令
返回值:成功无返回(执行指定程序去了),失败返回-1
该函数是通过 路径+程序名 来加载进程
案例:将ps aux打印的内容写到一个文件中
1 | //execl():是让子线程不执行父线程的代码,而是执行execl所指定的文件 |
3.4wait()函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态;如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个过程。
该函数的三个功能:
阻塞等待子进程退出;
回收子进程残留资源
获取子进程结束状态(退出原因)
pid_t wait(int *status);
参数:status是传出参数,结合系统提供的宏函数,可以得到子进程的一些信息
返回值:成功:回收子进程的id号;失败:-1(没有子进程)
可使用wait函数传出的参数status来保存进程的退出状态:
WIFEXITED(status)为非0,表示进程正常结束
- 如上正常退出,使用WEXITSTATUS(status)来获取进程的退出状态(正常的参数)
WIFSIGNALED(status)为非0,表示进程异常终止
- 如上异常退出,使用WTERMSIG(status)来获取使进程终止的那个信号的编号
WIFSTOPPED(status)为非0,表示进程处于暂停状态
- 如上为暂停状态,使用WSTOPSIG(status)来取得使进程暂停的那个信号的编号
WIFCONTINUED(status)为真,表示进程暂停后已经继续运行
1 | //测试:wait函数只有在子线程执行完后才会回收,然后继续向下执行;否则就阻塞在该处,等子线程执行完 |
1 | //测试:通过系统提供的宏函数来查看子线程的退出状态信息等 |
3.5waitpid()函数
pid_t waitpid(pid_t pid , int *status , int optains)
参数:
pid:指定某个子进程进行回收(大于0:回收指定ID的子进程;-1:回收任意子进程,相当于wait;0:回收和当前调用waitpid一个组的所有子进程;小于-1:回收指定进程组内的任意子进程)
status:是传出参数,结合系统提供的宏函数,可以得到子进程的一些信息
options:通过特定参数,可以完成特定功能,如WNOHANG指以非阻塞方式回收
返回值:
大于0:表示成功回收的子进程pid
等于0:参3指定了WNOHANG,并且没有子进程结束会返回0(非阻塞方式)
-1:失败,设置errno
waitpid()与wait()的区别是:waitpid能指定某个进程进行回收
注意:一次wait/waitpid函数调用,只能回收一个子进程
1 | //测试:指定回收某个子线程,且子线程有序打印 |
1 | //测试:回收多个子线程 |
4.IPC方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。现在常用的进程间通信方式有:
管道(使用最简单)
信号(开销最小)
共享映射区(无血缘关系)
本地套接字(最稳定)
4.1管道
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
其本质是一个伪文件(实为内核缓冲区)
由两个文件描述符引用,一个表示读端(可以有多个),一个表示写端
规定数据从管道的写端流入管道,借助内核缓冲区(4k)实现
1.管道的局限性:
数据不能进程自己写,自己读
管道中数据不可以反复读取,一旦读走,管道中不再存在
采用半双工通信方式,数据只能在单方向上流动
只能在有公共祖先的进程间使用管道。
2.常见的通信方式有:单工通信、半双工通信、全双工通信
3.pipe函数
int pipe(int fd[2]); —> 创建,并打开管道
参数:
fd[0]:读端
fd[1]:写端
返回值:成功:0;失败:-1,设置errno
4.管道的读写行为:
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:(A)管道写端被全部关闭,read返回0(表示读到文件结尾);(B)写端没有全部被关闭,read阻塞等待(不久的将来可能有数据写入),此时会让出CPU
写管道:
管道读端全部被关闭:进程异常终止(也可以使用SIGPIPE信号,使进程不终止)
管道读端没有全部关闭:若管道已满,write阻塞;若管道未满,write将数据写入,并返回实际写入的字节数。
1 | //测试pipe管道 |
案例:使用管道实现父子进程间通信,完成ls | wc -l,假定父进程实现ls,子进程实现wc
ls | wc -l 的含义是将ls命令的输出通过管道传递给wc -l命令,然后统计输出的行数。
1 | void sys_err(const char *str){ //负责出错时,打印错误 |
分析:ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc -l正常应该会从stdin读取数据,但此时会从管道的读端读。
案例:使用管道实现兄弟进程间通信,兄:ls,弟:wc -l,父:等待回收子进程。
1 | //用兄弟进程实现 |
4.2 FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程;但通过FIFO,不相关的进程也能交换数据。FIFO是Linux基础文件类型中的一种。但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信,并且可以有多个写端和多个读端,但读完就没有了。
创建管道方式:
终端:mkfifo 命名管道文件名
代码:int ret = mkfifo(“my_mkfifo”,0664);返回-1未成功创建
注意:可以通过终端mkfifo 命名管道文件名 来创建,也可以通过c程序
写端的代码如下:
1 | // 通过命名管道对两个无血缘关系的进程进行通信 |
读端的代码如下:
1 | // 通过命名管道对两个无血缘关系的进程进行通信 |
4.3存储映射I/O
存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样就可以在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现
1.创建映射区
void *mmap(void * addr , size_t length , int prot , int flags , int fd , off_t offset);
参数:
addr:指定映射区的首地址。通常传NULL,表示让系统自动分配
length:共享内存映射区的大小。(<=文件的实际大小)
prot:共享内存映射区的读写属性。PROT_READ、PROT_WRITE
flags:标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE(私有:对内存的操作不会反映到物理磁盘上)
fd:用于创建共享内存映射区的那个文件的文件描述符。
offset:默认0,表示映射文件全部。偏移位置需是4K的整数倍。
返回值:成功:映射区的首地址;失败:MAP_FAILED,设置errno
2.释放映射区
int munmap(void *addr , size_t length);
参数:
addr:mmap的返回值
length:大小
3.使用注意事项:
用于创建映射区的文件大小为0,实际指定非0大小创建映射区,出“总线错误”;
用于创建映射区的文件大小为0,实际指定0大小创建映射区,出“无效参数”;
用于创建映射区的文件读写属性为只读,映射区属性为读、写,出“无效参数”;
创建映射区,需要read权限(因为创建时,需要查看映射指定的文件);当访问权限指定为MAP_SHARED(共享)时,mmap的读写属性应该<=文件的open权限(只给mmap写属性不可以)–>因为共享时,对内存操作都会同样的对磁盘(文件)操作;
文件描述符fd,在mmap创建映射区完成即可关闭,后续访问文件用地址访问;
映射区访问权限为MAP_PRIVATE(私有)时,对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上
映射区访问权限为MAP_PRIVATE(私有)时,在需要open文件时,有读权限来创建映射区即可;
4.mmap函数的保险调用:
fd = open(“文件名”,O_RDWR);
mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
1 | //测试创建一个映射区 |
5.mmap进程通信
父子进程通信
- 父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
- MAP_PRIVATE(私有映射):父子进程各自独占映射区(修改,互相都看不到);
- MAP_SHARED(共享映射):父子进程共享映射区;
- 父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
无血缘关系间的进程通信
- 两个进程打开同一个文件(创建的映射区);一个进程写入,另外一个进程读出。
mmap:数据可以重复读取(当创建的映射区为4字节时,只要读的速度快于写的速度,那么可以重复读同一个正整数多次,直到新数据写入);而fifo只能读取数据一次。
1 | //父子进程实现mmap映射区之间的通信 |
5.信号
信号是一种软件中断,通知程序某种事件的发生。常见的信号有SIGABRT(当进程调用abort函数的时候自动发送), SIGALRM(当timer被触发的时候自动发送),等等。
5.1常识
1.信号的共性:简单、不能携带大量信息、满足条件才发送
2。信号的特质:信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。所有信号的产生及处理全部都是由内核完成的
3.产生信号
按键产生,如Ctrl+c、Ctrl+z、Ctrl+\
系统调用产生,如kill、raise、abort
软件条件产生,如定时器alarm
硬件异常产生,如非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
命令产生:如kill命令
4.信号的状态
递达:产生并且到达进程,可以直接被内核处理掉
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态
5.信号处理的方式:执行默认动作、忽略(丢弃)、捕抓(自定义)
6.阻塞信号集(信号屏蔽字):本质就是位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,再解除屏蔽前,就一直处于未决态
7.未决信号集:本质就是位图。用来记录信号的处理状态,该信号集中的信号表示已经产生,但尚未被处理
5.2信号四要素及常规信号
信号使用之前,应先确定其四要素,而后再用。即信号编号、信号名称、信号对应事件、信号默认处理动作。
1.常规信号:
1)SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程。
2)SIGINT:当用户按下了<Ctrl+c>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号,默认动作为终止进程。
3)SIGQUIT:当用户按下<Ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号,默认动作为终止进程。
5)SIGTRAP:该信号由断点指令或其他trap指令产生,默认动作为终止进程,并产生core文件。
6)SIGABRT:调用abort函数时产生该信号,默认动作为终止进程并产生core文件。
7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
8)SIGFPE:在发生致命的运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误,默认动作为终止进程并产生core文件。
9)SIGKILL:无条件终止进程,本信号不能被忽略处理和阻塞,默认动作为终止进程,它向系统管理员提供了可以杀死任何进程的方法。
10)SIGUSR1:用户定义的信号,即程序员可以在程序中定义并使用该信号,默认动作为终止进程。
11)SIGSEGV:指示进程进行了无效内存访问,默认动作为终止进程并产生core文件。
12)SIGUSR2:用户自定义信号,程序员可以在程序中定义并使用该信号,默认动作为终止进程。
13)SIGPIPE:Broken pipe向一个没有读端的管道写数据,默认动作为终止进程。
14)SIGALRM:定时器超时,超时的时间由系统调用alarm设置,默认动作为终止进程。
15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止,通常用来要示程序正常退出,执行shell命令kill时,缺省产生这个信号,默认动作为终止进程。
17)SIGCHLD:子进程状态发生变化时,父进程会收到这个信号,默认动作为忽略这个信号。
18)SIGCONT:如果进程已停止,则使其继续运行,默认动作为继续/忽略。
19)SIGSTOP:停止进程的执行,信号不能被忽略处理和阻塞,默认动作为暂停进程。
注意:只有每个信号所对应的事情发生了,该信号才会被递送(但不一定递达),不应该乱发信号。
2.kill函数与kill命令
- kill函数:给指定进程发送指定信号(不一定是杀死)
int kill(pid_t pid , int signum);
- 参数pid: >0:发送信号给指定进程。=0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。<-1:取绝对值,发送信号给该绝对值所对应的进程组的所有组员。=-1:发送信号给,有权限发送的所有进程。
1 | //测试用kill杀死子进程 |
kill命令:如杀死一个进程(kill -9 进程号)
5.3 alarm函数
设置定时器(闹钟),在指定seconds后,内核会给当前进程发送SIGALRM(14)信号,进程收到该信号,默认终止动作。
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds);
参数:定时秒数
返回值:上次定时剩余秒数,无错误现象。
常用:取消定时器alarm(0),返回旧闹钟剩余的秒数
注意:定时与进程状态无关,无论进程处于何种状态,alarm都计时。
1 | //用alarm测试1秒可以数多少次 |
[补]使用time命令查看程序执行的时间(如time ./alarm_count),alarm_count是上面代码的函数
得出:实际执行时间 = 系统时间+用户时间+等待时间(最多)—->程序运行的瓶颈在于IO,优化程序,首选优化IO。
5.4setitimer函数
setitimer函数可以替代alarm函数,精度到微妙,还可以实现周期定时
int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
参数:
new_value:定时秒数(结构体类型见代码)
old_value:传出参数,上次定时剩余时间
返回值:
- 成功:0;失败:-1,设置errno
提示:
it_interval:用来设定两次定时任务之间间隔时间
it_value:定时的时长
1 | void myfunc(int signo){ |
5.5信号集和未决信号集
1.信号集操作函数
自定义信号集(用于和mask发生或与关系):sigset_t set;
清空信号集(全置为0):sigemptyset(sigset_t *set);
信号集全置1:sigfillset(sigset_t *set);
将一个信号添加到集合中:sigaddset(sigset_t *set,int signum);
将一个信号从集合中移除:sigdelset(sigset_t *set,int signum);
判断一个信号是否在集合中(在是1,不在是0):sigismember(const sigset_t *set,int signum);
2.设置信号屏蔽字和解除屏蔽
int sigprocmask(int how , const sigset_t *set , sigset_t *oldset);
how:SIG_BLOCK是设置阻塞;SIG_UNBLOCK是取消阻塞;SIG_SETMASK是用自定义set替换mask
set:自定义set
oldset: 旧有的mask
用来屏蔽信号、解除屏蔽也使用该函数。其本质是读取或修改进程的信号屏蔽字(PCB中)
注意:屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略是表示将信号丢弃处理
3.查看未决信号集
int sigpending(sigset_t *set);
- set:传出的未决参数
1 | //对信号2设置了阻塞,并查看了未决信号集,Ctrl+c可以进行查看 |
5.6sigaction函数
signal函数和sigaction函数都是只注册一个信号的捕抓函数,捕抓是有内核来完成的
int sigaction (int signum , const struct sigaction *act , struct sigaction *oldact);
参数:
singum:捕抓的信号
act:传入参数,新的处理方式
oldact:传出参数,旧的处理方式
信号捕抓的特性:
进程正常运行时,默认PCB中有一个信号屏蔽字,假定为#,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉,捕捉到该信号之后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由#来指定,而是用sa_mask来指定,调用完信号处理函数,再恢复为#。
捕捉函数执行期间,本信号自动被屏蔽(sa_flgs=0)。
捕捉函数执行期间,被屏蔽信号多次发生,解除屏蔽后只处理一次
案例:用signal()函数对信号2进行捕抓
1 | void sys_err(const char *str){ |
案例:用sigaction()函数对信号2进行捕抓(可以设置多个函数的捕抓)
1 | void sys_err(const char *str){ |
案例:通过信号捕捉回收子进程(当一个时间点有多个进程死亡时,此时只能处理一个,其他死亡的子进程没有回收就会是僵尸进程)
1 | void sys_err(const char *str){ |
6.会话与守护进程
6.1进程组和会话
进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid和kill函数的参数中都曾使用过。操作系统设计的进程组的概念,是为了简化对多个进程的管理。当父进程,创建子进程的时候,默认子进程与父进程属于同一个进程组。进程组ID = 第一个进程ID(组长进程)。所以,组长进程标识,其进程组ID = 其进程ID。
而会话就是进程组的集合。
1.创建一个会话需要注意以下6点:
- 调用进程不能是进程组组长,该进程变成新会话的首进程
- 该进程成为一个新进程组的组长进程
- 需要root权限 (Ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 如果该进程调用的是组长进程,则出错返回
- 建立新会话时,先调用fork,父进程终止,子进程调用setsid()
2.getsid()函数:获取进程所属的会话ID
pid_t getsid(pid_t pid);
参数:pid为所需要获取的进程
返回值:成功(返回调用进程的会话ID);失败(返回-1,设置errno)
注意:组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程
3.setsid()函数:创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
pid_t setsid(void);
返回值:成功(返回调用进程的会话ID);失败(返回-1)
注意:调用setsid函数的进程,既是新的会长,也是新的组长。
1 | //子进程成立新会话(父进程不能创建会话) |
6.2守护进程
daemon进程。通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发送或周期性执行某一动作。不受用户登录注销影响,通常采用以d结尾的命名方式。
1.创建守护进程,最关键的一步是调用setsid函数,创建一个新的Session,并成为Session leader。
2.守护进程的创建步骤:
fork子进程,让父进程终止
子进程调用setsid()创建新会话
通常根据需要,改变工作目录位置chdir()
通常根据需要,重设umask文件权限掩码
通常根据需要,关闭/重定向文件描述符012
守护进程 业务逻辑。—->while()
1 | //守护进程的创建(运行在后台) |
7.线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程是独立调度和分派的基本单位,同一进程中的多条线程将共享该进程中的全部系统资源,但同一进程中的多个线程有各自的调用栈、寄存器环境和线程本地存储。
1.线程与进程区别:
进程:有独立的进程地址空间,有独立的pcb
线程:有独立的pcb,没有独立的进程地址空间—>是轻量级进程LWP
注意:线程是最小的执行单位;进程是最小分配资源单位,可看成是只有一个线程的进程。进程里创建线程后,进程也叫线程了
2.查看某个进程里的线程:
- ps -Lf 进程pid:线程号是LWP那一栏(相当于进程号,线程号是接在进程号后面的)
3.线程共享资源:文件描述符、每种信号的处理的方式、当前工作目录、用户ID和组ID、内存地址空间(.text/.data/.bss/heap/共享库)、全局变量;
4.线程非共享资源:线程id、处理器现场和栈指针(内核栈)、独立的栈空间(用户空间栈)、errno变量、信号屏蔽字、调度优先级
5.线程优缺点
优点:提高程序并发性、开销小、数据通信、共享数据方便;
缺点:是库函数,不稳定、调试、编写困难、对信号支持不好
7.1线程控制原语
1.线程函数
pthread_t pthread_self(void);
返回值:本线程id号
作用:获取线程id,线程id是在进程地址空间内部用来标识线程身份的
注意:线程ID是在进程中来标识线程身份的,进程通过线程ID来对其加以区分;而LWP是线程号,标识线程身份给CPU用的(CPU用线程号来划分时间片)
2.创建线程
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void*),void *arg);
参1:传出参数,表示新创建的子线程id
参2:线程属性,传NULL表示使用默认属性
参3:子线程回调函数,若创建成功,pthread_create函数返回时,该函数会被自动调用
参4:参数3函数的参数,没有的话就传NULL
返回值:成功(返回0);失败(errno)
注意:主函数利用pthread_create()创建新线程时,主函数会立即返回值并执行下面代码,不会因为新线程没有执行完回调函数而阻塞。
当新线程启动后,它会在后台执行回调函数,同时主线程继续执行自己的任务。如果回调函数执行时间较长,主线程仍然不会被阻塞,它会继续往下执行,直到遇到需要等待新线程完成任务的代码段。
3.退出当前线程
void pthread_exit(void *retval);
- 参数:退出值,无退出值时,NULL
注意:exit()是退出当前进程;return;是返回到调用者那里去
pthread_exit(NULL)的作用是在线程中显式地退出线程的函数调用。它用于终止当前线程的执行,并将线程的退出状态设置为NULL。当调用pthread_exit(NULL)时,当前线程会立即退出,并将控制返回给创建该线程的线程。这意味着线程的执行会终止,但其他线程仍然可以继续执行。
4.阻塞等待线程退出,获取线程退出状态
int pthread_join(pthread_t thread,void** retval);
参1:要回收的线程id
参2:传出参数,存储线程退出的状态,线程正常退出,得到来自该线程在回调函数中返回的信息;线程异常退出,返回-1.
返回值:成功回收(返回0),失败(errno)
注意:在进程中,进程结束是exit(1),退出状态是int型,所以回收进程wait的参数是
int*
型;在线程中,线程结束是pthread_exit(void*)
,退出状态是void*
型,所以回收线程pthread_join参数是void**
型。
5.杀死(取消)线程,类似于进程中的kill()
int pthread_cancel(pthread_t thread);
参数:待杀死的线程id
返回值:成功(0);失败(errno)
注意:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
如果子线程没有到达取消点,那么使用pthread_cancel无效。我们可以在程序中。手动添加一个取消点,使用pthread_testcancel()。
取消点:通常是一些系统调用creat,open,pause,close,read,write。(可以理解为是线程有进入内核,如果线程执行的内容是if,while,for这些则不能用pthread_cancel()杀死)
案例:终止线程的三种方法,注意取消点的概念。
6.线程出错要用这个函数打印
char *strerror(int errnum);
- 使用:fprintf(stderr,”pthread_join error:%s”,strerror(ret));
7.实现线程分离
int pthread_detach(pthread_t thread);
参数:待分离的线程id
返回值:成功(0),失败(errno)
注意:pthread_detach不能与pthread_join一起用,当使用线程分离时,被分离的子线程就不归主线程管了,子线程执行完由系统回收,不需要主线程再调用pthread_join来回收,如果调用,会回收失败。
8.线程属性,用来设置分离属性
pthread_attr_t attr; //创建一个线程属性结构体变量
pthread_attr_init(&attr); //初始化线程属性
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //设置
pthread_create(&tid,&attr,tfn,NULL); //设置线程属性,创建为分离态
pthread_attr_destroy(&attr); //销毁线程属性
案例:创建一个线程执行回调函数
1 | //编译时:后面要加-pthread |
案例:循环创建多个子线程
1 | //编译时:最后面要加-pthread |
案例:使用pthread_exit()函数来退出子线程
1 | //编译时:最后面要加-pthread |
案例:通过pthread_join()回收子线程
1 | struct thrd{ |
案例:通过pthread_cancel()终止子线程
1 | void *tfn(void *arg){ |
案例:通过pthread_detach()设置线程分离
1 | void *tfn(void *arg){ |
案例:通过线程属性设置分离属性
1 | //通过线程属性设置来使子线程分离,这样就可以创建一个是一个了,而不需要后面手动一个一个设置 |
案例:终止线程的三种方法,注意取消点的概念。
1 | void *tfn1(void *arg){ |
7.2线程使用注意事项
线程使用注意事项:
主线程退出其他线程不退出,主线程应该调用pthread_exit()
避免僵尸线程,使用pthread_join、pthread_detach、pthread_create(指定分离属性)
malloc和mmap申请的内存可以被其他线程释放(线程共享堆)
应该避免在多线程模型中调用fork。除非马上exec。因为如果调用fork,则子进程中只有调用fork的线程存在,其他线程在子进程中均默认被pthread_exit
信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
8.线程同步
协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
锁的使用:建议锁,对公共数据进行保护。所有线程应该在访问公共数据前先拿锁再访问。但锁本身不具备强制性。
8.1互斥锁mutex
1.使用互斥锁的一般步骤:
pthread_mutex_t lock; //创建锁
pthread_mutex_init; //初始化
pthread_mutex_lock; //加锁
访问共享数据(stdout)
pthread_mutex_unlock(); //解锁
pthread_mutex_destroy; //销毁锁
补:restrict关键字:用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
2.初始化互斥锁的两种方法:
pthread_mutex_t mutex; //定义一把锁
pthread_mutex_t_init(&mutex,NULL); //动态初始化
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; //静态初始化
注意:尽量保证锁的粒度(范围),越小越好。访问共享数据前加锁,访问结束立即解锁。
方便记忆:互斥锁,本质是结构体。我们可以看成整数,初值为1,即pthread_mutex_init()调用成功;加锁理解为–操作,阻塞线程;解锁理解为++操作,唤醒阻塞在锁上的线程。
try锁:尝试加锁,成功–操作;失败就返回,同时设置错误号EBUSY。
案例:通过互斥锁进行对公共区域的访问
1 | pthread_mutex_t mutex; //定义一把互斥锁 |
8.2读写锁
锁只有一把,以读方式给数据加锁为读锁;以写方式给数据加锁为写锁。相较于互斥量(互斥锁)而言,当读线程多的时候,读写锁可以提高访问效率。
1.读写锁的特性
读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞
读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞
读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求,优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。即写独占,读共享
2.主要应用函数
pthread_rwlock_init //自定义读写锁
pthread_rwlock_destroy //销毁读写锁
pthread_rwlock_rdlock //读模式加锁
pthread_rwlock_wrlock //写模式加锁
pthread_rwlock_tryrdlock //尝试读模式加锁
pthread_rwlock_trywrlock //尝试写模式加锁
pthread_rwlock_unlock //读写锁解锁(通用)
以上函数的返回值都是:成功(返回0);失败(返回错误号)
pthread_rwlock_t rwlock //定义一个读写锁变量rwlock
3.死锁
是使用锁不恰当导致的现象,如:
对一个锁反复lock
两个线程,各自持有一把锁,请求另一把
案例:模拟3个线程不定时写同一个全局资源,5个线程不定时读同一个全局资源
1 | int counter; //定义一个全局变量 |
8.3条件变量
条件变量本身不是锁,但它也可以造成线程阻塞,通常与互斥锁配合使用。
1.主要应用函数:
pthread_cond_init函数 //初始化条件变量
pthread_cond_destroy函数 //销毁条件变量
pthread_cond_wait函数 //等待条件满足
pthread_cond_timewait函数 //等待条件满足(超时不等)
pthread_cond_signal函数 //唤醒阻塞在条件变量的线程(一个)
pthread_cond_broadcast函数 //唤醒阻塞在条件变量的线程(多个)
以上函数返回值:成功(返回0);失败(返回错误号)
pthread_cond_t cond; //定义一个条件变量cond
2.初始化条件变量的两种方法:
pthread_cond_t cond; //定义一个条件变量
pthread_cond_t_init(&cond,NULL); //动态初始化
pthread_cond_t cond=PTHREAD_MUTEX_INITIALIZER; //静态初始化
3.pthread_cond_wait函数作用:
阻塞等待条件变量cond(参1)满足
释放已掌握的互斥锁,相当于pthread_mutex_unlock(&mutex)。
注意:上面两步为一个原子操作(中间不会分开执行)
- 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex)
案例:模拟一个消费者-生产者模式
1 | void err_thread(int ret,char*str){ //线程创建失败调用的打印错误信息的函数 |
9.信号量
应用于线程、进程间同步,因为互斥锁对于多个线程访问同一个公共内存空间时,只能一个一个访问,虽然保证了数据正确性的目的,但导致了线程的并发性下降。
信号量是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
1.主要应用函数
sem_init //信号量初始化
sem_destroy //信号量销毁
sem_wait //信号量加锁
sem_trywait
sem_timedwait
sem_post //信号量解锁
以上函数返回值:成功(返回0);失败(-1)
2.int sem_init(sem_t *sem,int pshared,unsigned int value);
参1:信号量
参2:0—>用于线程间同步;1—>用于进程间同步
参3:N值(指定同时访问的线程数)
3.sem_wait():一次调用,做一次–操作,当信号量的值为0时,再次–就会阻塞(对比pthread_mutex_lock)
4.sem_post():一次调用,做一次++操作,当信号量的值为N时,再次++就会阻塞(对比pthread_mutex_unlock)
案例:用信号量实现生产者-消费者模式
1 |
|