Linux操作系统

基本概念

操作系统

操作系统(operating system,简称os)是管理计算机硬件与软件资源的计算机程序,计算机系统由处理器、内存、磁盘、键盘、显示器、网络接口以及各种其它输入/输出设备组成。计算机是一个复杂的系统,如果每个程序员都需要掌握系统的所有细节,那就不可能再编写代码,所以计算机安装了一层软件,称作操作系统,它的任务是为用户程序提供一个更好、更简单、更清晰的计算机模型。
在这里插入图片描述

内核

内核,操作系统的核心,作为应用程序连接硬件设备的桥梁,应用程序只关心与内核交互,不用关心硬件的细节。
内核的功能:

  • 进程调度:管理进程、线程,决定哪个进程、线程使用CPU
  • 内存管理:决定内存的分配和回收
  • 提供文件系统:提供文件系统,允许对文件执行创建、获取、更新以及删除等操作
  • 管理硬件设备:为进程与硬件设备之间提供通信能力
  • 提供系统调用接口:进程可利用内核入口点(也称为系统调用)请求内核去执行各种任务

内核具有很高的权限,可以控制CPU、内存、硬盘等硬件,而应用程序具有的权限较小,因此大多数操作系统把内核分为两个区域:

  • 内核空间,只有内核程序可以访问
  • 用户空间,专门给应用程序使用

CPU可以在两种状态下运行:用户态和内存态,在用户态下运行时,CPU只能访问用户空间的内存;在内核态运行时,CPU既能访问用户空间也能访问内核空间。

库函数和系统调用

库函数:写好的函数,一般放在lib文件中。
系统调用:内核的入口,用户程序只能在用户态下运行,如果需要进入内核空间,就需要通过系统调用,当应用程序使用系统调用时,会产生一个中断,cpu中断当前执行的用户程序,跳转到中断处理程序,也就是开始执行内核程序。内核处理完后主动触发中断,把cpu执行权限交回给用户程序。
在这里插入图片描述

程序与进程

  • 程序:使用变成语言编写的代码经过编译和链接处理,得到的计算机可以理解和执行的指令
  • 进程:进程是正在执行的程序,是操作系统资源分配和调度的基本单位。在内核角度,进程是一个个实体,内核需要他们之间分享各种计算机资源,例如内存资源,内核回在开始为进程分配一定大小的内存,并统筹该进程和整个系统对内存的需求,对这一分配进行跳转,程序终止时,内核会释放所有资源,供其他进程使用。

虚拟内存

进程中只能访问虚拟内存地址,操作系统会把虚拟内存地址翻译成真实的内存地址。这种内存管理方式称之为虚拟内存。
进程如果访问的是真实的物理地址,多个进程之间会互相干扰。例如一个程序在一个物理地址上写入了一个新值,可能将另一个程序在这个位置的内容擦掉,程序就会崩溃。
操作系统为每个进程分配一套独立的虚拟地址,每个进程访问自己的虚拟地址,互不干涉,操作系统会提供将虚拟内存地址和物理内存地址映射的机制。

并发

并发,一个时间段中多个进程都处于已启动运行到运行完毕之间的状态。但任意一个时刻点上只有一个进程在运行。

文件IO

文件描述符

以文件描述符代指打开的文件,(unsigned int)。一个进程启动后默认打开三个文件描述符:0标准输入,1标准输出,2标准错误
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
操作系统为每个进程维护好一个文件描述符表,新打开文件返回文件描述符表中尉使用的最小文件描述符,调用open函数可以打开或创建一个文件,得到一个文件描述符。

#include<unistd.h>//系统调用头文件
#include<fcntl.h>//系统调用和宏的头文件
int main(int argc,char*argv[]){
	int fd=open("./a.txt",O_RDONLY);
	printf("%d",fd);
}

open和close函数

open:打开或新建一个文件
函数原型:

int open(const char*pathname,int flags);
int open(const char*pathname,int flags,mode_t mode);

函数参数:

  • pathname:要打开或创建的文件路径
  • flags:文件访问模式,选择多个时使用 | 连接
    必选项:三选一
    O_RDONLY:只读打开
    O_WRONLY:只写打开
    O_RDWR:可读可写打开
    常用项:可追加0个或多个,使用按位或连接
    O_APPEND:追加,如果文件已有内容,之策打开我呢见缩写的数据依附到文件的末尾而不覆盖原来的内容
    O_CREAT:文件不存在时创建,使用此选项时需要提供第三个参数mode,表示该文件的访问权限,ugo的rwx,一般使用八进制数位表示,例如0644,需要注意文件最终权限:mode&~umask
    O_EXCL:如果同时指定了O_CREAT,并且文件已存在,则出错返回,一般用于测试是文件是否存在
    O_TRUNC:如果文件已存在,将其长度截断为字节
    O_NONBLOCK:设置非阻塞模式
    普通文件默认是非阻塞的,内核缓冲区保证了普通文件I/O不会阻塞,打开普通文件一般会忽略该参数
    设备、管道和套接字默认是阻塞的,以O_NONBLOCK方式打开可以 做非阻塞I/O(NONBLOCK I/O)

函数返回值:

  • 成功:返回一个最小且未被占用的描述符
  • 失败:返回-,并设置errno值

close函数:关闭文件
函数原型:

int close(int fd);

函数参数:fd 文件描述符
函数返回值:

  • 成功返回0
  • 失败返回-1,并设置errno值

需要说明的是当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不断用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符需要手动维护避免造成大量文件描述符和系统资源的浪费

read和write

函数描述:从打开的设备或文件中读取数据
函数原型:

ssize_t read(int fd,void*buf,size_t count);

函数参数:

  • fd:文件描述符
  • buf:读取的数据保存在缓冲区buf
  • count:buf缓冲区存放的最大字节数

函数返回值:

  • 0:读取到的字节数

  • =0:文件读取完毕
  • -1:出错,并设置errno

write函数
函数描述:向打开的设备或文件中写数据
函数原型:

ssize_t write(int fd,const void*buf,size_t count);

函数参数:

  • if:文件描述符
  • buf:缓冲区,要写入文件或设备的数据
  • count:buf中数据的长度

函数返回值

  • 成功:返回写入的字节数
  • 错误:返回-1并设置errno

lseek函数

所有打开的文件都有一个当前文件偏移量(current file offset),也叫读写偏移量和指针。文件偏移量通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数(下一个read()或write()操作的文件起始位置),文件的第一个字节的偏移量为0
文件打开时,会将文件偏移量设置为指向文件开始(使用O_APPEND除外),以后每次read()和write()会自动对其调整,以指向已读或已写数据的下一字节,因此连续的read()和write()将顺序递进,对文件进行操作。
使用lseek函数可以改变文件的偏移量
函数描述:移动文件指针
头文件:

#include<sys/types.h>
#include<unistd.h>

函数原型:

off_t lseek(int fd,off_t offset,int whence);

函数参数:

  • fd:文件描述符
  • offset:字节数,以whence参数为基点解释offset
  • whence:解释offset参数的基点
    SEEK_SET:文件偏移量设置为offset
    SEEK_CUR:文件偏移量设置为当前文件偏移量加上offset,offset可以为负数
    SEEK_END:文件偏移量色湖之为文件长度加上offset,offset可以为负数

函数返回值:

  • 若lseek成功执行,则返回新的偏移量
  • 失败返回-1并设置errno

lseek常用操作

  • 文件指针移动到头部
    lseek(fd,0,SEEK_SET);
  • 获取文件指针当前位置
    int len=lseek(fd,0,SEEK_CUR);
  • 获取文件长度
    int len=lseek(fd,0,SEEK_END);
  • lseek实现文件拓展
    lseek(fd,n,SEEK_END);//扩展n个字节
    write();//扩展后需要指向一次写操作才能扩展成功

主函数传参:argc是参数个数,argv接收参数,第一个参数是函数名

阻塞与非阻塞

  • 阻塞:调用函数的结果返回前,当前进程(线程)会被挂起,直到得到结果才会继续运行。
  • 非阻塞:调用函数后会直接返回结果,不会等待结果

open()打开文件时可以通过参数O_NONBLOCK设置文件为非阻塞模式

  • 普通文件默认是非阻塞的,内核缓冲区保证了普通文件I/O不会发生阻塞事件,所以普通文件不需要考虑是否阻塞
  • 设备、管道和套接字默认是阻塞的,以O_NONBLOCK方式打开可以做非阻塞I/O (NONBLOCK I/O)

阻塞模式标准输入

char buf[1024];
int read_count=read(0,buf,sizeof(buf));
printf("%d\n",read_count);
write(1,buf,read_count);

非阻塞模式

int fd=open("/dev/fd/0",O_RDWR|O_NONBLOCK);
char buf[1024];
int read_count=read(fd,buf,sizeof(buf));
printf("%d\n",read_count);
write(fd,buf,read_count);

fcntl函数

函数描述:对打开的文件描述符进行控制,如获取或修改打开文件的状态标志(对应open函数的flags参数)
函数原型:int fcntl(int fd,int cmd,…/arg*/);
函数参数:

  • fd:要控制的文件描述符
  • cmd:不同值对应不同的操作
    cmd为F_GETFL:获取文件描述符的flag值
    cmd为F_SETFL:设置文件描述符的flag值
    cmd为F_DUPFD:复制文件描述符的flag值,与dup函数功能相同

函数返回值:返回值取决于cmd

  • 成功
  • 若cmd为F_DUPTD,返回一个新的文件描述符
  • 若cmd为F_GETFL,返回文件描述符的flag值
  • 若cmd为F_SETFL,返回0
  • 失败返回-1,并设置errno值

fcntl函数常见的操作

  • 获取文件的属性标志
    int flags=fcntl(fd,F_GETFL);
  • 修改文件状态标志(只允许修改某些标志如:O_APPEND、O_NONBLOCK)
    flag=flag|O_NONBLOCK;
    fcntl(fd,F_SETFL,flag);
  • 复制文件描述符,使用大于等于startfd的最小未使用量作为新的文件描述符
    int newfd=fcntl(fd,F_DUPFD,startfd);

dup和dup2函数

实际上文件描述符和打开的文件之间并不一定是一对一的关系也可能是多对一的关系,多个文件描述符可以指向同一打开文件。

dup函数
函数描述:复制文件描述符返回复制后的文件描述符
函数原型:int dup(int oldfd);
函数参数: oldfd 要复制的文件描述符
函数返回值:

  • 成功:返回最小且没占用的文件描述符
  • 失败:返回-1,设置errno值

dup2函数
函数描述:复制文件描述符,指定复制后的文件描述符
函数原型:int dup2(int oldfd,int newfd);
函数参数:

  • oldfd:原来的文件描述符
  • newfd:复制后的新的文件描述符,如果newfd已经指向了一个文件,先关闭原来打开的文件,再将newfd指向oldfd指向的文件

函数返回值

  • 成功:将oldfd复制给newfd,两个文件描述符指向同一个文件
  • 失败:返回-1,设置errno值

perror和strerror函数

perror函数:打印errno值对应的报错信息
头文件:#include<stdio.h>
函数原型:void perror(const char*s);
函数参数:s 一个字符串,用于在输出报错信息时添加一些额外信息

strperror函数:将错误码转换为相应的错误信息字符串
头文件:#include<string.h>
函数原型:char*strerror(int errnum);
函数参数:errnum 错误码
函数返回值:返回包含错误信息的字符串

stat/lstat/fstat函数

获取文件属性,lstat和stat的区别在于如果文件是符号链接,返回的文件属性是符号链接本身,fstat则是指定文件描述符获取文件信息
头文件:#include<sys/stat.h>
函数原型:

int stat(const char*pathname,struct stat*statbuf);
int lstat(const char*pathname,struct stat*statbuf);
int fstat(int fd,struct stat*statbuf);

函数参数:

  • pathname:要获取属性的文件路径
  • statbuf:传出参数,指向struct stat结构体的指针,用于存储获取到的文件信息
  • fd:要获取属性的文件描述符

函数返回值:

  • 成功返回0
  • 失败返回-1
struct stat{
	dev_t st_dev;   //文件所在的设备号
	ino_t st_ino;   //inode号
	mode_t st_mode; //文件类型及权限
	nlink_t st_nlink; //硬链接计数
	uid_t st_uid;  //拥有者
	gid_t st_gid;  //所属用户组
	dev_t st_rdev;  //如果是设备文件时有效,为设备号
	off_t st_size;  //文件的字节数
	blksize_t st_blksize;  //文件系统中block的大小
	blkcnt_t st_blocks;  //文件所占扇区数量
	time_t st_atime;  //最后访问时间
	time_t st_mtime;  //最后修改时间
	time_t st_ctime;  //最后改变状态时间
};

在这里插入图片描述

在这里插入图片描述

进程

进程状态

当一个程序开始运行时,他可能会经历以下几种状态

  • 运行态:运行态指进程实际占用CPU时间片运行时的状态
  • 就绪态:就绪态指的是可运行,但因为其它进程正在运行而处于就绪状态
  • 阻塞态:该进程正在等待某一事件发生(如输入/输出操作的完成)而暂时停止运行,这时即使给它cpu控制权,它也无法运行

在这里插入图片描述
状态切换

  • 运行态到阻塞态:当进程遇到某个事件需要等待时会进入阻塞态
  • 阻塞态到就绪态:当进程要等待的事件完成时会从阻塞态变到就绪态
  • 就绪态到运行态:处于就绪态的进程被操作系统的进程调度程序选中后,就分配到cpu开始运行
  • 运行态到就绪态:进程运行过程中,分配给它的时间片用完后,操作系统会将其变为就绪态,接着从就绪态的进程中选择一个运行

程序调度指的是,决定哪个进程优先被运行和运行多久,已经设计出许多算法尝试平衡系统整体效率与各个进程之间的竞争需求

进程控制块PCB

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。其内部成员很多,重点部分:

  • 进程id,每个进程有唯一的id,用pid_t类型表示,其实就是一个非负整数
  • 进程的状态,有就绪、运行、挂起状态
  • 描述虚拟地址空间的信息
  • 文件描述符表,包含很多指向file结构体的指针
  • 进程切换时需要保存和恢复的一些cpu寄存器
  • 描述控制终端的信息
  • 当前工作目录
  • umask掩码
  • 和信号相关的信息
  • 用户id和用户组id
  • 会话(session)和进程组
  • 进程可以使用的资源上限

创建进程

创建进程的方式:

  • 系统初始化:启动操作系统时会创建若干个进程
  • 用户请求创建:例如双击图标启动程序
  • 系统调用创建:一个运行的进程可以发出系统调用创建新的进程帮助其完成工作

fork函数:创建进程,新创建的是当前进程的子进程
函数原型:pid_t fork(void);
函数返回值:

  • 成功:
    父进程:返回新创建的子进程id
    子进程:返回0
  • 失败返回-1,子进程不被创建

fork()函数会创建一个子进程,父进程的内容会复制到子进程的进程空间中,包括父进程的数据断喝堆栈段,并且和父进程共享代码段,所以成功后父子进程都停留在了进程创建函数fork上,因此,fork()函数在父子进程中都会返回,两个返回值不同
父子进程间遵循读时共享写时复制的原则。现在的linux内核在fork()函数时往往在创建子进程时并不立即创建父进程的数据段和堆栈段,而是当子进程修改这些数据内容时复制操作才会发生,内核才会给子进程分配空间,将父进程的内容复制过来,然后继续后边的操作,这样的实现更加合理,对于那些只是为了复制自身完成一些工作的进程来说,这样做的效率会更高这也是现代操作系统的一个重要的概念“写时复制”的一个重要体现

父子进程的局部变量、全局变量、堆区空间不是共享的,父子进程打印变量的地址是相同的(该地址是虚拟地址空间),但是他们指向的物理空间是不同的

父子进程间文件共享,执行fork()子进程会获得父进程所有文件描述符的副本,这些副本的创建类似于dup(),同一文件描述符在父子进程中对应的是相同的文件

终止进程

进程终止方式:

  • 正常退出:
    从main函数返回
    调用exit()或_exit(),exit()是库函数,_exit()是系统调用,程序一般不直接调用_exit(),而是调用库函数exit()
  • 异常退出:
    被信号终止

exit函数:结束进程
头文件:#include<stdlib.h>
函数原型:void exit(int status);
函数参数:进程的退出状态,0表示正常退出,非0值表示因异常退出,保存在全局变量$ ?中,$ ?保存的是最近一次运行的进程的返回值,返回值有以下3种情况

  • 程序中的main函数运行结束,$ ?中保存main的函数返回值
  • 程序运行中调用exit函数结束运行,$ ?中保存exit函数的参数
  • 程序异常退出$ ?中保异常出错的错误号

孤儿进程

孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,变成孤儿进程后会有一个专门用于回收的init进程成为它的父进程,称init进程领养孤儿进程

僵尸进程

孤儿进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程。
需要注意的是僵尸不能用kill命令清除,因为kill命令只是用来终止进程的,而僵尸进程已经终止。可以杀死他的父进程,让init进程变成它的父进程,init进程可以回收它

wait函数

函数描述:父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)

头文件:

#include<sys/types.h>
#include<sys/wait.h>

函数原型:pid_t wait(int *status);
函数返回值:

  • 成功:返回清理掉的子进程id
  • 失败:返回-1

当进程终止时,操作系统的隐式回收机制会:

  • 关闭所有文件描述符
  • 释放用户空间、分配的内存,内核的PCB仍存在,其中保存该进程的退出状态。(正常退出->退出值;异常退出->终止信号)

可使用wait函数传出参数status来保存进程的退出状态,借助宏函数来进一步判断进程终止的具体原因,宏函数可以分为如下三组:

1
WIFEXITED(status)//为真-》进程正常结束
WEXITSTATUS(status)//如上宏为真,获取进程退出状态(exit的参数)
2
WIFSIGNALED(status)//为真-》进程异常终止
WTERMSIG(status)//如上宏为真,得使进程终止的那个信号的编号
3
WIFSTOPPED(status)//为真-》进程处于暂停状态
WSTOPSIG(status)//如上宏为真,取得使进程暂停的那个信号的编号
WIFCONTINUED(status)//如上宏为真,进程暂停后已经继续运行

waitpid函数

函数描述:作用同wait,但可指定进程id为pid的进程清理,可以不阻塞
头文件:

#include<sys/types.h>
#include<sys/wait.h>

函数原型:pid_t waitpid(pid_t pid,int *status,int options);
函数参数:
参数pid:

  • pid>0:回收指定id的子进程
  • pid=-1:回收任意子进程(相当于wait)
  • pid=0:回收和当前调用进程(父进程)一个组的任一子进程
  • pid<-1:设置福德进程组id,回收指定进程组内的任意子进程

函数返回值:

  • 成功:返回清理掉的子进程id
  • 失败:返回-1(无子进程)
  • 参数3为WNOHANG,非阻塞,且子进程正在运行返回0

注意:一次wait或waitpid调用只能清理掉一个子进程,清理多个子进程应该使用循环

exec函数族

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序,当进程调用一种exec函数时,通过该调用,进程能以全新程序来替换当前运行的程序,将当前进程的代码段和数据段替换为所要加载的程序的代码段和数据段,然后让进程从新的代码段的第一条指令开始执行,但进程id不变,换核不换壳
其实有六种以exec开头的函数,统称exec函数:

#include<unistd.h>
int execl(const char *pathname,const char *arg,....);
int execlp(const char *file,const char *arg,....);
int execle(const char *pathname,const char *arg,...,char *const envp[]);
int execv(const char *pathname,const char *arg[]);
int execvp(const char *file,const char *arg[]);
int execvpe(const char *file,const char *arg[]);

execl函数:加载一个进程,通过路径+程序名来加载
函数参数:

  • pathname:可执行文件的路径
  • arg:可执行程序的参数,对应main()函数的第二个参数(argv),格式相同,以NULL结束

函数返回值:

  • 成功:无返回
  • 失败:-1

execlp函数:加载一个进程,借助PATH环境变量
函数参数:

  • file可执行文件的文件名,系统会在环境变量PATH的目录列表中寻找可执行文件
  • arg:可执行程序的参数

函数的返回值:

  • 成功:无返回
  • 失败:-1

该函数通常用来调用系统程序,如:ls,date,cp,cat等命令

进程间通信

进程间通信简称IPC,进程间通信就是在不同进程之间传播或交换信息
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的
各个进程之间若想是心啊通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域
在这里插入图片描述
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件,内核缓冲等)。由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它发生了某种事件,比如进程终止时需要通知其父进程(信号)
  • 进程控制:有些进程希望控制另一个进程的执行(如debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够即使直到它的状态改变

管道

管道是unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个管道

匿名管道

匿名管道用于进程间的通信,且仅限于本地关联进程之间的通信
进程间通信的本质就是,让不同的进程看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信

在这里插入图片描述
注意:
这里父子进程看到的同一份文件资源是由操作系统进行维护的,所以当父子进程对该文件进行写入操作时,该进程缓冲区当中的数据并不会进行写时拷贝
管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到硬盘中,因为这样做有IO参与会降低效率,而且也没有必要,也就是说这种文件是一批不会把数据写入磁盘当中的文件,换句话说磁盘文件和内存并不一定是一 一对应的,有些文件只会在内存中存在,而不会在磁盘中存在

pipe函数:创建匿名管道
函数原型:int pipe(int pipefd[2]);
函数参数:pipefd是一个传入参数,用于返回两个指向管道读端和写端的文件描述符
pipefd[0]:管道读端的文件描述符
pipefd[1]:管道写端的文件描述符

函数返回值:

  • 成功:返回0
  • 失败:返回-1,设置errno

在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下
1.父进程调用pipe函数创建管道
2.父进程创建子进程
3.父进程关闭写端,子进程关闭读端

注意:管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
从管道写端写入的数据会被存到内核缓冲,直到从管道的读端被读取

管道读写情况:

  • 管道中没有数据:write返回成功写入的字节数,读端进程阻塞在read上
  • 管道中有数据没满:write返回成功写入的字节数,read返回读到的字节数
  • 管道已满:写端进程阻塞在write上,read返回读到的字节数
  • 写端全部关闭:read正常读,返回读到的字节数(没有数据返回0,不阻塞)
  • 读端全部关闭:写端进程write会异常终止进程(被SIGPIPE信号杀死)

命名管道

匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常一个管道只能由一个进程创建,然后该进程调用fork,以后父子进程之间就看可以使用该管道,如果要实现两个毫不相关进程之间的通信可以使用命名管道来实现,命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一管道文件进行资源共享
创建命名管道文件:mkfifo 管道名
在这里插入图片描述
mkfifo函数

  • 函数描述:程序中创建命名管道
  • 头文件:
    #include<sys/types.h>
    #include<sys/stat.h>
  • 函数原型:int mkfifo(const char *pathname,mode_t mode);
  • 函数参数:
    pathname:表示要创建的命名管道文件
    mode:表示创建命名管道文件的默认权限
  • 函数返回值:
    成功:返回0
    失败:返回-1

命名管道的打开规则:

  • 读进程打开fifo,并且没有写进程打开时:
    没有O_NONBLOCK:阻塞直到有写进程打开该fifo
    由O_NONBLOCK:立刻返回成功
  • 写进程打开fifo,并且没有读进程打开时:
    没有O_NONBLOCK:阻塞直到有读进程打开该fifo
    有O_NONBLOCK:立刻返回失败,错误码为ENXIO

内存映射

内存映射是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件
映射分为两种:

  • 文件映射:将文件的一部分映射到调用进程的虚拟文件中,,对文件映射部分的访问转化为对相应内存区域的字节操作,映射页面会按需自动从文件中加载
  • 匿名映射:一个匿名映射没有对应的文件,其映射页面的内容会被初始化为0,一个进程所映射的内存可以与其它进程的映射共享,共享的两种方式:
    两个进程对同一文件的同一区域映射
    fork()创建的子进程继承其父进程的映射
    在这里插入图片描述
    mmap()函数
    函数描述:在调用进程的虚拟地址空间中创建一个新内存映射
    头文件:<sys/mman.h>
    函数原型: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
    MAP_SHARED:创建共享映射,对映射的写入会写入文件里,其它共享映射的进程可见
    MAP_PRAVITE:创建私有映射,对映射的写入会写入文件里,其它映射进程不可见
    MAP_ANONYMOUS:创建匿名映射,此时会忽略参数fd(设为-1),不涉及文件,没有血缘关系的进程不能共享
    fd:要映射的文件描述符,匿名映射设为-1
    offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小(4k)的整数倍

函数返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno中

munmap()函数
函数描述:接触映射区域
头文件:<sys/mman.h>
函数原型:int munmap(void *addr,size_t lengh);
函数参数:

  • addr:指向要解除映射的内存起始地址
  • length:解除映射的长度

消息队列

消息队列是保存再内核中的消息链表,消息对罗列是面向消息进行通信的,一次读取一条完整的消息,每条消息中还包含一个整数表示优先级,可以根据优先级读取消息,进程A可以往队列中写入消息,进程B读取消息,并且,进程A写入消息后就可以终止,进程B在需要的时候再去读取
在这里插入图片描述
每条消息通常具有以下属性:

  • 一个表示优先级的整数
  • 消息数据部分的长度
  • 消息数据本身

消息队列函数
头文件

  • #include<fcntl.h>
  • #include<sys/stat.h>
  • #include<mqueue.h>

打开和关闭消息队列:

  • mqd_t mq_open(const char*name,int oflag);
  • mqd_ mq_open(const char*name,int oflag,mode_t mode,struct mq_ *atrr);
  • int mq_close(mqd_t mqdes);

获取和设置消息队列属性:

  • int mq_getattr(mqd_t mqdes,struct mq_attr *attr);
  • int mq_setattr(mqd_t mqdes,const struct mq_attr *newattr,struct mq_attr *oldattr);

在队列中写入和读取一条消息

  • int mq_send(mqd_t mqdes,const char*msg_ptr,size_t msg_len,unsigned int msg_prio);
  • ssize_t mq_receive(mqd_t mqdes,char*msg_ptr,size_t msg_len,unsigned int *msg_prio);

删除消息队列:int mq_unlink(const char*name);
函数参数和返回值:

  • name:消息队列名

  • oflag:打开方式,类似open函数
    必选项:O_RDONLY,O_WRONLY,O_RDWR
    可选项:O_NONBLOCK,O_CREAT,O_EXCL

  • mode:访问权限,oflag中含有O__CREAT且消息队列不存在时提供该参数

  • attr:队列属性,open时传null表示默认属性

  • mqdes:表示消息队列描述符

  • msg_ptr:指向缓冲区的指针

  • msg_len:缓冲区大小

  • msg_prio:消息优先级

返回值:

  • 成功返回0,open返回消息队列描述符,mq_receive返回写入成功字节数
  • 失败返回-1

注意:在编译时报undefined reference to mq_open,undefined reference to mq_close时,除了要包含头文件#include #include外,还需要加上编译选项-lrt

消息队列的关闭与删除
使用mq_close函数关闭消息队列,关闭后消息队列并不从系统中删除,一个进程结束,会自动调用关闭打开着的消息队列
使用mq_unlink函数删除一个消息队列名,使当前进程无法使用该消息队列,并将队列标记为在所有进程关闭该队列后删除该队列
posix消息队列具备随内核的持续性,所以即使当前没有该进程打开着某个消息队列,该队列及其上的消息也将一直存在,直到调用mq_unlink并最后一个进程关闭该消息队列时,将会被删除,操作系统会维护一个消息队列的引用计数,并记录有多少进程正在使用该消息队列,只有当引用计数减为0时,消息队列才会被删除

消息队列属性
每个消息队列有四个属性

  • mq_getattr 返回所有这些属性
  • mq_setattr设置mq_flags属性,其它成员忽略
  • mq_open可以指定mq_maxmsg和mq_msgsize属性,其它成员忽略
struct mq_attr{
	long mq_flags;//是否阻塞
	long mq_maxmsg;//最大消息数
	long mq_msgsize;//最大消息大小
	long mq_curmsgs;//当前消息个数
};

线程

创建线程

启动程序时,产生的进程只有单条线程称之为(initial)或主(main)线程,函数pthread_create()负责创建一条新线程,新线程通过调用带有参数arg的函数start_routine(即start_routine(arg))而开始执行,调用pthread_create()的线程会执行该调用之后的语句
进程内部的每个线程都有一个唯一标识,称之为线程id,线程获取自己的线程id通过pthread_self()函数
使用ps -eLf 查看线程
ubuntu20编译有线程的代码可能会失败,使用gcc编译需要加上参数-lpthread
pthread_create函数
函数描述:创建一个新线程,类似进程中的fork()函数
头文件:#include<pthread.h>
函数原型:
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void ※(※star_routine)(void※),
void *arg);

函数参数

  • thread:传出参数,保存系统为我们分配好的线程id
  • attr:通常传null,表示使用线程默认属性,若想使用具体属性也可以修改该参数
  • start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束
  • arg:线程主函数的参数

函数返回值:

  • 成功返回0
  • 失败返回错误号

pthread_self函数
函数描述:获取线程id,类似于进程中的getpid()函数,线程id是进程内部的识别标识(两个进程间,允许线程id相同)
函数原型:pthread_t pthread_self(void);

  • 函数返回值:成功返回线程id,pthread_t为无符号整数(%lu)
  • 失败无

终止线程

终止线程的方式:

  • 线程start_routine函数执行return
  • 线程调用pthread_exit()终止线程
  • 调用pthread_cancel(thread)取消指定线程
  • 任意线程调用了exit(),或者主线程执行了return语句(在main()函数中),都会导致进程中的线程立即终止

pthread_exit()函数将终止调用线程,且其返回值可由另一线程通过调用pthread_join来获取,调用pthread_exit相当于线程的start_routine函数中执行return,不同之处在于,可以在线程start_routine函数所调用的任意函数中调用pthread_exit()都能够直接终止线程
pthread_exit函数
函数描述:终止线程
函数原型:void pthread_exit(void *retval);
函数参数:retval:表示线程退出状态,通常传null

pthread_cancel函数
函数描述:向指定线程发送一个取消请求,发出取消请求后,函数pthread_cancel()当即返回,不会等待目标线程的退出,被请求取消的线程不会立即取消,需要等待到达某个取消点,取消点通常是一些系统调用,也可以使用pthread_testcancel函数手动创建一个取消点,被取消的线程返回值是pthread_canceled (-1)
函数原型:int pthread_cancel(pthread_t thread);
函数参数:thread 要取消的进程id
函数返回值:

  • 成功返回0
  • 失败返回错误号

线程的连接与分离

pthread_join函数
函数描述:等待指定线程终止并回收,这种操作叫连接(joining),未连接的线程会产生僵尸线程,类似僵尸进程的概念
函数原型:int thread_join(pthread_t pthread,void **retval);
函数参数:

  • thread:要等待的线程id
  • retval:存储线程返回值

函数返回值:

  • 成功返回0
  • 失败返回错误号

pthread_datach函数
函数描述:默认线程是可连接的,当线程退出时,其它线程可以通过调用pthread_join获取其返回状态,有时程序员并不关心线程的返回状态,只是希望系统在线程终止时能够自动清理并移除,这时可以调用pthread_detach函数,将线程标记为分离状态
函数原型:int pthread_detach(pthread_t pthread);
函数参数:

  • thread:要分离的线程id
  • retval:存储线程返回值

函数返回值:

  • 成功返回0
  • 失败返回错误号

线程同步

子线程没有独立的地址空间,大部分数据都是共享的如果同时访问数据,就会造成混乱,所以要进行控制,线程之间要协调好先后执行顺序

互斥量(互斥锁)

线程的主要优势在于能够通过全局变量来共享信息,这种便捷的共享是有代价的,必须确保多个线程不会同时修改同一变量,或者同一线程不会读取正由其它线程修改的变量
互斥量可以保护对共享变量的访问
pthread_mutex_init函数
函数描述:初始化一个互斥量
函数原型:int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr);
函数参数:

  • mutex:指向互斥量的指针,下面几个函数都有该参数
  • mutexattr:指向定义互斥量属性的指针,取默认值传null

函数返回值:

  • 成功返回0
  • 失败返回错误号

pthread_mutex_lock和pthread_mutex_unlock函数
函数描述:给互斥量加锁或解锁,解锁的函数在解锁的同时幻想阻塞在该互斥量上的线程,默认先阻塞的先唤醒

函数原型:

  • int pthread_mutex_lock(pthread_mutex_t *mutex);
  • int pthread_mutex_unlock(pthread_mutex_t *mutex);

函数返回值:

  • 成功返回0
  • 出现错误返回错误号,加锁不成功,线程阻塞

pthread_destroy函数
函数描述:销毁一个互斥量
函数原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);
函数返回值:

  • 成功返回0
  • 失败返回错误号

pthread_mutex_trylock函数
函数描述:尝试给互斥量加锁,加锁不成功直接返回错误号(EBUSY),不会阻塞,其它与pthread_mutex_lock相同

死锁现象

当多个线程中为了保护多个共享资源而使用了多个互斥锁,如果多个互斥锁使用不当,就可能造成,多个线程一致等待对方的锁释放,这就是死锁现象

在这里插入图片描述
死锁产生的四个必要条件:

  • 互斥条件:资源只能同时被一个进程占用
  • 持有并等待条件,线程1已经持有资源A,可以申请持有资源B,如果资源B已经被线程2持有,这时线程1持有资源并等待资源B
  • 不可剥夺条件,一个线程持有资源,只能自己释放后其它线程才能使用,其它线程不能强制回收该资源
  • 环路等待条件,多个线程互相等待资源,形成一个环形等待链

避免死锁:破坏其中一个必要条件就可以避免死锁,常用方法如下:

  • 锁的粒度控制:破坏请求与保持条件,尽可能减少持有锁的时间,降低发生死锁的可能性
  • 资源有序分配:破坏环路等待条件,规定线程使用资源的顺序,按规定顺序给资源加锁
  • 重试机制:持有并等待条件,如果尝试获取资源失败,放弃已持有的资源后重试

读写锁

对写锁由读锁和写锁两部分组成,读取资源时用读锁,修改资源时用写锁,其特性为:写独占,读共享(读优先锁),读写锁适合读多写少的场景

读写锁的工作原理:

  • 没有线程持有写锁时,所有线程都可以一起持有读锁
  • 有线程持有写锁时,所有的读锁和写锁都会阻塞

读优先锁,有线程持有锁,这时有一个读线程和一个写线程想要获取锁,读线程会优先获取锁就是读优先锁,反过来就是写优先锁

读写锁函数

  • int pthread_rwlock_init(pthread-rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
  • int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
  • int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
  • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

场景分析

  • 持有读锁时,申请读锁,全部直接加锁成功,不需要等待
  • 持有写锁时,申请写锁,申请的写锁阻塞等待,写锁释放再申请加锁
  • 持有读锁时,申请写锁,写锁阻塞
  • 持有写锁时,申请读锁,读锁阻塞
  • 持有写锁时,申请写锁和读锁,申请的读锁和写锁都会阻塞,当持有的写锁释放时,读锁先加锁成功
  • 持有读锁时,申请写锁和读锁,申请的写锁阻塞,读锁加锁成功,写锁阻塞到读锁全部解锁才能加锁,在此期间可能一直有读锁申请,会导致写锁一直无法请求成功,造成饥荒

条件变量

条件变量,通知状态的改变,条件变量允许一个线程就某个共享变量的状态变化通知其它线程,并让其它线程等待(阻塞于)这一通知
条件变量总是结合互斥量使用,条件变量就共享变量的状态改变发出通知,而互斥量则提供对该共享变量访问的互斥

条件变量函数

  • int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
    参数attr表示条件变量的属性,默认传null
  • int pthread_cond_destroy(pthread_cond_t *cond);
  • int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
    阻塞等待一个条件变量,释放持有的互斥锁,这两步是原子操作
    被唤醒时,函数返回,解除阻塞并重新申请获取互斥锁
  • int pthread_cond_signal(pthread_cond_t *cond);
    唤醒一个阻塞在条件变量上的线程
  • int pthread_cond_broadcast(pthread_cond_t *cond);
    唤醒全部阻塞在条件变量上的线程
  • int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime);
    限时等待一个条件变量
    参数abstime是一个timespec结构体,以秒和纳秒表示绝对时间

生产者消费者模型
线程同步典型案例即为生产者消费者模型,而借助条件变量实现这一模型,是比较常见的一种方法,假定有两个线程一个模拟生产者行为,一个模拟消费者行为,两个线程同时操作一个共享资源(一般称之为汇聚),生产者向其中台南佳产品,消费者从中消费掉产品
在这里插入图片描述
相较于互斥量而言,条件变量也可以减少竞争,如直接使用互斥量,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥量是无意义的,有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争,提高了程序效率
场景:一个线程产生随机数放入链表中,一个线程从链表中取出一个随机数打印

信号量

信号量是操作系统提供的一种协调共享资源访问的方法,信号量是由内核维护的整型变量(sem)它的值表示可用资源的数量
对信号量的原子操作

  • p操作
    如果可用资源(sem>0),占用一个资源(sem-1)
    如果可用资源(sem=0),进程或线程阻塞,直到有资源
  • v操作
    如果进程或线程在等待资源,释放一个资源(sem+1)
    如果有进程或线程在等待资源,唤醒一个进程或线程

p操作或v操作成对出现,进入临界区前进行p操作,离开临界区后进行v操作

在这里插入图片描述
posix提供两种信号量,命名信号量和无名信号量,命名信号量一般是用于进程间同步,无名信号量一般用在线程间同步
命名信号量和无名信号量通用函数

  • int sem_wait(sem_t *sem);
    p操作,信号量大于0或信号量-1,否则进程阻塞
  • int sem_post(sem_t *sem);
    v操作,有阻塞进程会唤醒阻塞进程,否则信号量+1
  • int sem_getvalue(sem_t *sem,int *sval);

无名信号量函数

  • int sem_init(sem_t *sem,int pthread,unsigned int value);
    sem:要进行初始化的信号量
    pshared:等于0用于同一进程下多线程的同步
    pshared:大于0用于多个相关进程间的同步(即fork产生的)
    value:信号量的初始值
  • int sem_destroy(sem_t *sem);

命名信号量函数:

  • sem_t sem_open(const char *name,int oflag,mode_t mode,unsigned int value);
    打开或创建信号量
    参数name:信号量名字
    参数flag:0,打开信号量,O_CREAT,如果name不存在就创建一个信号量,O_CREAT|O_EXCL,如果name存在,会失败
    参数mode和value:参数oflag有O_CREAT时,需要传这两个参数,mode代表权限,value代表信号量的初始值
    返回值:指向sem_t值的指针,后续通过这个指针操作新打开或创建的信号量
  • sem_close(sem_t *sem);
  • int sem_unlink(const char *name);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值