Linux系统编程
2020-09-28 15:54:59 1 举报
AI智能生成
Linux系统编程
作者其他创作
大纲/内容
文件操作
stat函数
获取文件属性(从inode上获取)
返回值
成功:0
失败:-1
文件属性
struct stat { dev_t st_dev; //文件的设备编号 ino_t st_ino; //节点 mode_t st_mode; //文件的类型和存取的权限 nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1 uid_t st_uid; //用户ID gid_t st_gid; //组ID dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号 off_t st_size; //文件字节数(文件大小) blksize_t st_blksize; //块大小(文件系统的I/O 缓冲区大小) blkcnt_t st_blocks; //块数 time_t st_atime; //最后一次访问时间 time_t st_mtime; //最后一次修改时间 time_t st_ctime; //最后一次改变时间(指属性)};
st_mode
该变量占2byte共16位
掩码的使用:st_mode&掩码
其他人的权限(0-2bit)
掩码:S_IRWXO 00007 过滤st_mode中除其他人权限以外的信息
S_IROTH 00004 读权限
S_IWOTH 00002 写掩码
S_IXOTH 00001 执行权限
所属组权限(3-5bit)
掩码:S_IRWXG 00070 过滤 st_mode中除所属组权限以外的信息
S_IRGRP 00040 读权限
S_IWGRP 00020 写权限
S_IXGRP 00010 执行权限
文件所有者权限(6-8bit)
掩码:S_IRWXU 00700 过滤 st_mode中除文件所有者权限以外的信息
S_IRUSR 00040 读权限
S_IWUSR 00200 写权限
S_IXUSR 00100 执行权限
特殊权限位(9-11bit)
很少用
S_ISUID 0004000 设置用户ID
S_ISGID 0002000 设置组ID
S_ISVTX 0001000 黏住位
文件类型(12-15bit)
掩码:S_IFMT 0170000 过滤 st_mode中除文件类型以外的信息
S_IFSOCK 0140000 套接字
S_IFLNK 0120000 符号链接(软链接)
S_IFREG 0100000 普通文件
S_IFBLK 0060000 块设备
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符设备
S_IFIFO 0010000 管道
特性
能够穿透(跟踪)符号链接
vi编辑器
lstat函数
不穿透(跟踪)符号链接
ls -l
rm
access函数
作用
测试指定文件是否拥有某种权限
原型
参数
pathname-->文件名
mode-->权限类别
R_OK是否有读权限
W_OK是否有写权限
X_OK是否有执行权限
F_OK测试一个文件是否存在
0-->所有欲查核的权限都通过了检查
-1-->有权限被禁止
chmod函数
改变文件权限
filename-->文件名
pmode-->权限
必须是一个8进制数
0-->改变成功
-1-->失败
chown函数
改变文件的所有者
0-->成功
truncate函数
将参数path指定的文件大小改为参数length指定的大小,如果原来的文件大小比参数length大,则超过部分会被删去
path-->文件路径
length-->指定的文件大小
0-->执行成功
-1-->执行失败
链接
link函数
创建一个硬链接
symlink函数
作用:创建一个软链接
readlink函数
作用:读软链接对应的文件名,不是读内容
unlink函数
删除一个文件的目录项并减少它的链接数,若成功则返回0,否则返回-1,错误原因存于error
如果想通过这个函数来成功删除文件,你就必须拥有这个文件的所属目录的写和执行权限
使用
1.如果是符号链接,删除符号链接
2.如果是硬链接,硬链接数减1,当减为0时,释放数据块和inode
3.如果文件硬链接数为0.但有进程已打开该文件,并持有文件描述符。则等该进程关闭该文件时,kernel才真正去删除该文件
利用该特性创建临时文件时,先open或creat创建一个文件,马上unlink此文件
rename函数
作用:文件重命名
头文件:stdio.h
进程
相关概念
程序和进程
程序(剧本,纸)
指编译好的二进制文件,在磁盘上,不占用系统资源
进程(戏)
一个抽象的概念,与操作系统原理联系紧密,进程是活跃的程序,占用系统资源
同一个剧本可以在多个舞台上同时上演;同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)如:同时打开两个终端。各自都有一个bash但彼此ID不同
并发
在操作系统中,一个时间段中有多个程序都处于已启动运行到运行完毕之间的状态,但,任一个时刻点上仍只有一个进程在运行
分时复用CPU
单道和多道程序设计
单道程序设计
多道程序设计
在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。
时钟中断即为多道程序设计模型的理论基础。 并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。
在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。实质上,并发是宏观并行,微观串行!
CPU和MMU
进程控制块PCB
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
查看struct task_struct 结构体定义,/usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件其内部成员有很多。
进程id
系统中每个进程都有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数
进程状态
进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起(阻塞)态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
进程切换时需要保存和恢复的一些CPU寄存器
描述虚拟地址空间的信息
umask掩码
文件描述符,包含很多指向file结构体的指针
和信号相关的信息
会话(session)个进程组
进程可以使用的资源上限
ulimit -a
环境变量
概念
指在操作系统中用来指定操作系统运行环境的一些参数
特征
字符串(本质)
有统一格式:名=值[:值]
值用来描述进程环境信息
大多数name由大写字母加下划线组成
存储形式
与命令行参数类似
char *[]数组
数组名environ,内部存储字符串,NULL作为哨兵结尾
使用形式
加载位置
与命令行参数类似,位于用户区,高于stack起始位置
引入环境变量表
须声明环境变量
entern char ** environ
常见的环境变量
PATH
可执行文件的搜索路径
SHELL
当前shell,值通常为/bin/bash
TERM
当前终端类型
在图形界面终端下值通常为xterm
终端类型决定了一些程序的输出显示方式
比如图形界面终端可以显示汉字,而字符终端一般不行
LANG
语言和locale
决定了字符编码以及时间、货币等信息的显示格式
HOME
当前用户目录的路径
很多程序需要在主目录下保存配置文件使得每个用户在运行程序时都有自己的一套配置
常用函数
getenv函数
获取环境变量值
char *getenv(const char *name);
成功:返回环境变量的值
失败:NULL(name不存在)
setenv函数
设置环境变量的值
参数overwrite取值
1:覆盖原环境变量
0:不覆盖该参数常用于设置新环境变量
unsetenv函数
删除环境变量name的定义
int unsetenv(const char *name);
注意事项
name不参在仍返回0(成功)
当name命名为”ABC=“时则会出错
进程控制
fork函数
用途
创建一个子进程
pid_t fork(void);
pid_t类型表示进程ID,但为了表示-1,它是有符号整型(0不是有效进程ID,init最小,为1)
成功:1.父进程返回子进程的ID(非负) 2.子进程返回0
注意
不是fork函数能返回两个值而是fork后,fork函数变为两个,父子各自返回一个
循环创建n个子进程
一次fork函数调用创建一个子进程,那么创建n个呢简单想,for(int i = 0; i < n; i++){fork()};即可,实际上如图
从上图我们可以很清晰的看到,当n为3时候,循环创建了(2^n)-1个子进程,而不是N的子进程。需要在循环的过程,保证子进程不再执行fork ,因此当(fork() == 0)时,子进程应该立即break;才正确。
getpid函数
获取当前进程ID
pid_t getpid(void);\t
getppid函数
获取当前进程的父进程ID
pid_t getppid(void);
getuid函数geteuid函数
获取当前进程实际用户ID获取当前进程有效用户ID
uid_t getuid(void);uid_t geteuid(void);
getgid函数getegid函数
获取当前进程使用用户组ID获取当前进程有效用户组ID
gid_t getgid(void);gid_t getegid(void);
进程共享
1.父子进程之间在fork后。有哪些相同,那些相异之处呢?刚fork之后:父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
2.似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
重点注意
躲避父子进程共享全局变量的知识误区
父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)
特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
exec函数族(6种)
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
本质
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳
execlp函数
加载一个进程,借助PATH环境变量\t(l表示list,p表示PATH)
参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
用法
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
execlp(\"ls\
execl函数
加载一个程序,通过路径+程序名来加载
execl(\"/bin/ls\
execvp函数
加载一个进程,使用自定义环境变量env
变参形式
1.。。。
2.argv[ ](main函数也是变参函数,传入一个数组)
变参终制条件
1.NULL结尾
2.固参指定
一般规律
exec函数一旦调用成功即执行新的程序,不返回。只有失败返回-1;所以通常我们直接再exec函数调用后直接调用perror和exit,无需if判断
函数字母含义
l(list):命令行参数列表
p(path):搜索file时使用path变量
v(vector):使用命令行参数数组
e(environment):使用环境变量数组,不使用进程原来的环境变量
各函数之间的关系
事实上只有execve是真正的函数调用,其他五个函数最终调用execve
回收子进程
孤儿进程
父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,成为init进程领养孤儿进程
僵尸进程
进程终止,父进程尚未回收,子进程参与资源(PCB)存放于内核中,变成僵尸进程
特别注意,僵尸进程是不能用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止
wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。
这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
1.阻塞等待子进程退出
2.回收子进程残留资源
3.获取子进程结束状态(退出原因)
原型:pid_t wait(int *status); \t成功:清理掉的子进程ID;失败:-1 (没有子进程)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1.WIFEXITED(status)为非0→进程正常结束WEXITSTATUS(status)如上宏为真,使用此宏→获取进程退出状态(exit的参数)
2.WIFSIGNALED(status)为非0→进程异常终止WTERMSIG(status)如上宏为真,使用此宏→取得使进程终止的那个信号的编号
3.WIFSTOPED(status)为非0→进程处于暂停状态WSTOPSIG(status)如上宏为真,使用此宏→取得使进程暂停的那个信号的编号WIFCONTINUED(status)为真→进程暂停后已经继续运行
waitpid函数
作用同wait,但可指定pid进程清理,可以不阻塞
特殊参数和返回情况
参数pid
>0:回收指定ID的子进程
-1:回收任意子进程(相当于wait)
0:回收和当前调用waitpid一个组的所有子进程
<-1:回收指定进程组内的任意子进程
获取status
WIFEXITED(status)子进程正常exit终止,返回真 WEXITSTATUS(status)返回子进程正常退出值
WIFSIGNALED(status)子进程被信号终止,返回真 WTERMSIG(status)返回终止子进程的信号值
WIFSTOPPED(status)子进程被停止,返回真 WSTOPSIG(status)返回停止子进程的信号值
WIFCONTINUED(status)
options:返回0
WNOHANG没有子进程结束,立即返回
WUNTRACED如果子进程由于被停止产生的SIGCHLD,waitpid则立即返回
WCONTINUED如果子进程由于被SIGCONT唤醒而产生的SIGCHLD,waitpid则立即返回
一次wait或者waitpid调用只能清理一个子进程,清理多个子进程应使用循环
守护进程
daemon进程,通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互,周期性的等待某个事件发生或周期性执行某一动作不受用户登录注销影响。通常采用以d结尾的命名方式
创建步骤
1.fork子进程,让父进程终止
2.子进程调用setsid()创建新会话
3.通常根据需要,改变工作目录位置
4.通常根据需要,重设umask文件权限掩码
5.通常根据需要,关闭/重定向 文件描述符
6.守护进程 业务逻辑, while()
信号
基本概念
概念及特性
信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。1. 简单 2. 不能携带大量信息 3. 满足某个特设条件才发送。
机制
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
特质
由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
相关事件和状态
产生信号(5种方式)
1.按键产生
ctrl+c、ctrl+z、ctrl+\\等
2.系统调用产生
kill、raise、abort等
3.软件条件产生
定时器alarm
4.硬件异常产生
非法访问内存(段错误)、除0(浮点数例外)、内存对齐错误(总线错误)等
5.命令产生
kill命令等
递达
递送并到达进程
未决
产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态
信号处理方式
1.执行默认动作
2.忽略(丢弃)
3.捕捉(调用户处理函数)
信号集
存在于内核的PCB进程控制块种
阻塞信号集(信号屏蔽字)
将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
未决信号集
1. 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
2. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号四要素
1.编号
kill -l命令可查看
1-31常规信号
34-64实时信号
2.名称
宏名(可屏蔽平台差异)
3.事件
4.默认处理动作
term:终止进程
lgn:忽略信号(默认即是该信号忽略操作)
core:终止进程,生成core文件(查验进程死亡原因,用于gdb调试)
stop:停止(暂停)进程
cont:继续运行进程
特别强调
9) SIGKILL 和 19) SIGSTOP信号,不允许屏蔽、忽略和捕捉,只能执行默认动作。
只有每个信号所对应的事件发生了,信号才应被发送(但不一定递达),不应乱发信号
产生信号
终端按键产生信号
Ctrl + c → 2) SIGINT(终止/中断)\t \"INT\" ----Interrupt
Ctrl + z → 20) SIGTSTP(暂停/停止) \"T\" ----Terminal 终端。
Ctrl + \\ → 3) SIGQUIT(退出)
硬件异常产生信号
除0操作 → 8) SIGFPE (浮点数例外)\t\"F\" -----float 浮点数。
非法访问内存 → 11) SIGSEGV (段错误)
总线错误 → 7) SIGBUS\t
kill函数/命令产生信号
kill命令产生信号:kill -SIGKILL pid
kill函数:给指定进程发送指定信号(不一定杀死)
成功:0;失败:-1,设置errno
ID非法,信号非法,普通用户杀init进程等权级问题
sig
使用宏名,不同操作系统信号编号有出入
pid
>0:发送信号给指定进程
=0:发送信号给与调用kill函数继承属于同一进程组的所有进程
<0:取|pid|发给对应进程组
=-1:发送给进程有权限发送的系统中的所有进程
raise和abort函数
raise函数
int raise(int sig);成功:0 失败:非0值
abort函数
给自己发送异常终止信号 6)SIGABRT信号,终止并产生core文件
void abort(void); 无返回
软件条件产生信号
alarm函数
设置定时器(闹钟)在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds);
定时秒数
剩余秒数(无失败)
alarm(0)取消定时
使用time命令查看程序执行的时间程序运行的瓶颈在于IO,优化程序,首选优化IO
实际执行时间 = 系统时间 + 用户时间 + 等待时间
setitimer函数
设置定时器。可替代alarm函数,精度微妙,可以实现周期定时
which:指定定时方式
① 自然定时:ITIMER_REAL → 14)SIGLARM\t\t\t\t \t\t计算自然时间
② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM \t 只计算进程占用cpu的时间
③ 运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF\t\t 计算占用cpu及执行系统调用的时间
new_value:定时时间
struct itimerval结构体
it_interval
定时时隔时长
tv_sec:秒
tv_usec:微妙
it_value
定时时长
old_value:剩余时间
失败:-1,设置error
信号集操作函数
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的
信号集设定
sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
int sigemptyset(sigset_t *set);\t\t\t将某个信号集清0\t\t \t\t成功:0;失败:-1
int sigfillset(sigset_t *set);\t\t\t\t将某个信号集置1\t\t \t\t成功:0;失败:-1
sigprocmask函数
用来屏蔽信号、解除屏蔽其本质,读取或修改进程的信号屏蔽字(PCB中)
严格注意:屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。
set
传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset
传出参数,保存旧的信号屏蔽集。
how
1.\tSIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
2.\tSIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
3.\tSIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending函数
读取当前进程的未决信号集
int sigpending(sigset_t *set);\tset传出参数。 返回值:成功:0;失败:-1,设置errno
信号捕捉
signal函数
注册一个信号捕捉函数
typedef void (*sighandler_t)(int);
由于再不同的操作系统及版本下有不同行为,尽量避免使用
sigaction函数
修改信号处理动作在Linux中用来注册一个信号的捕捉函数
信号编号
act:传入参数,新的处理方式
oldact:传出参数,旧的处理方式
struct sigaction结构体
sa_handler
捕捉函数名
SIG_IGN表忽略
SIG_DEL表默认动作
sa_sigaction
当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
sa_mask
捕捉函数执行期间屏蔽字(临时生效)
sa_flags
通常设置为0,表使用默认属性
SA_SIGINFO:选用sa_sigaction来指定捕捉函数
SA_INTERRURT:系统调用被信号中断后,不重启
SA_RESTART:自动重启
SA_DEFER:不自动屏蔽本信号
。。。
信号捕捉特性
捕捉函数执行期间,屏蔽字有指定
默认,捕捉函数执行期间,被捕捉信号自动屏蔽sa_flag=0
常规信号不支持排队,多次产生只记录一次
内核实现信号捕捉过程
回调捕捉函数
调用结束先返回内核
可/不可重入函数
一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。
示例
显然,insert函数是不可重入函数,重入调用,会导致意外结果呈现。究其原因,是该函数内部实现使用了全局变量
1.\t定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free
2.\t信号捕捉函数应设计为可重入函数
3.信号处理程序可以调用的可重入函数可参阅man 7 signal
4.\t没有包含在上述列表中的函数大多是不可重入的,其原因为:a)\t使用静态数据结构b)\t调用了malloc或freec)\t是标准I/O函数
SIGCHLD信号
产生条件
子进程终止时
子进程接受到SIGSTOP信号停止时
子进程处于停止态,接受到SIGCONT后唤醒
借助该信号回收子进程
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
子进程结束status处理方式主要依赖于waitpad函数
注意问题
1.子进程继承了父进程的信号屏蔽字和信号处理动作 但子进程没有继承未决信号集spending
2.注意注册信号捕捉函数的位置
3.应该再fork之前,阻塞SIGCHLD信号, 注册完捕捉函数后解除阻塞
中断系统调用
慢速系统调用
可能会使进程永远阻塞的一类。如果再阻塞期间收到一个信号,该系统调用就被中断,不再继续执行;也可以设定系统调用是否重启。如:read、write、pause、wait...
其他系统调用
getpid、getppid、fork...
线程同步
线程同步概念
协同步调,按预定先后次序执行
与时间有关的错误
数据混乱
资源共享
调度随机
缺乏必要的同步机制
多个控制流访问同一共享资源
互斥量(互斥锁)
mutex
建议锁(协同锁)
锁,不会限制资源访问
线程不按规矩访问数据依然成功,会出现数据混乱
函数
pthread_mutex_t类型
本质:结构体
简化:1、0
失败:错误号
pthread_mutex_init
初始化一把互斥锁
互斥锁mutex(传出)
互斥锁属性attr(传入)
默认属性NULL
restrict关键字
所有修改该指针指向内存中内容的操作,只能通过本指针完成
静态初始化
pthead_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_destroy
销毁一个互斥锁
互斥锁&mutex
pthread_mutex_lock
加锁
mutex--(或-1)
pthread_mutex_unlock
解锁
mutex++(或+1)
同时将阻塞在该锁上的所有线程全部唤醒
pthread_mutex_trylock
非阻塞加锁
锁的\"粒度\"(临界区)
越小越好
再访问共享资源前加锁,访问结束后立即解锁
死锁
产生原因
对同一个互斥量重复加锁
持有锁A的线程1请求锁B,持有锁B的线程2请求锁A
振荡
避免方法
保证资源的获取顺序,要求每个线程获取资源的顺序一致
当得不到所有所需资源时,放弃已经获得的资源,等待
读写锁
使用要领
读共享,写独占
写锁优先级高
状态
读模式下加锁状态(读锁)
写模式下加锁状态(写锁)
不加锁状态
\"写模式加锁\"
解锁前,所有对该锁加锁的线程都会被阻塞
\"读模式加锁\"
如果线程以读模式对其加锁会成功
如果线程以写模式加锁会阻塞
既有试图以写模式加锁的线程,也有试图以读模式加锁的线程,优先满足写模式锁
使用场景
适合于对数据结构读的次数远大于写
pthread_rwlock_t类型
全部传&rwlock
pthread_rwlock_init
&rwlock
读写锁attr(NULL)
pthread_rwlock_destroy
pthread_rwlock_rdlock
pthread_rwlock_wrlock
pthread_rwlock_tryrdlock
pthread_rwlock_trywrlock
pthread_rwlock_unlock
条件变量
不是锁
可以造成线程阻塞
与mutex配合使用
pthread_cond_t类型
pthread_cond_init
&cond
条件变量attr(NULL)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_destroy
pthread_cond_wait
阻塞等待一个条件变量
&mutex
1.阻塞等待条件变量cond
2.释放已经掌握的互斥锁mutex
3.被唤醒时重新申请获取互斥锁
1.2步为原子操作
pthread_cond_timedwait
参3:abstime
绝对时间
struct timespec结构体
tv_sec秒
tv_nsec纳秒
查看man sem_timedwait函数,查看struct timespec结构体
使用方法
1.time_t cur = time(NULL);
2.struct timespec t;
3.t.tv_sec = cur + 1;
pthread_cond_signal
唤醒(至少)一个阻塞在条件变量上的线程
pthread_cond_broadcast
唤醒全部阻塞在条件变量上的线程
生产者消费者模型
优点:减少不必要的竞争
信号量
sem
进化版互斥锁(1-->N)
保证同步的同时,提高并发
sem_t类型
本质结构体
N代表线程数量
信号量的初值,决定了占用信号量的线程的个数
N不能<0
#include<semaphore.h>
sem_init
&sem
是否在进程间共享
非0进程间
PTHREAD_PROCESS_SHARED
0线程间
PTHREAD_PROCESS_PRIVATE
N线程数
sem_destroy
sem_wait
给信号量加锁sem--
++/--用函数来操作
sem_trywait
sem_timedwait
采用绝对时间
sem_post
给信号量解锁sem++
唤醒阻塞在信号量上的线程
进程间同步
互斥量
互斥量属性设置函数
pthread_mutexattr_t类型
pthread_mutexattr_init
pthread_mutexattr_destroy
pthread_mutexattr_setpshared
参2:pshared取值:\t\t\t线程锁:PTHREAD_PROCESS_PRIVATE (mutex的默认属性即为线程锁,进程间私有)\t\t\t进程锁:PTHREAD_PROCESS_SHARED
pthread_mutexattr_getpshared
文件锁
fcntl函数
参数1
文件描述符
参数2
F_SETLK(struct flock *)设置文件锁(trylock)
F_SETLKW (struct flock *) 设置文件锁(lock)W --> wait
F_GETLK (struct flock *)获取文件锁
参数3
struct flock结构体
l_type:锁的类型
F_RDLCK
F_WRLCK
F_UNLCK解锁
l_whence:偏移位置
SEEK_SET
SEEK_CUR
SEEK_END
l_start:起始偏移
l_len:长度
0表示整个文件加锁
l_pid:持有该锁的进程ID
仅限F_GETLK中使用
Linux系统编程
系统I/O原理
C库IO函数工作流程
库函数与系统函数的关系
FD-->文件描述符FP_POS-->文件指针BUFFER-->缓冲区(c库函数维护)
区分一个函数是“系统函数”还是“库函数”依据:
1.是否访问内核数据结构
2.是否访问外部硬件资源
二者有其一就为系统函数二者均无则为库函数
虚拟地址空间
PCB与文件描述符
系统I/O函数
系统调用函数都必须考虑返回值
open函数
pathname:文件的相对或绝对路径
flags:打开方式
必选项(互斥)
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR可读可写打开
可选项
O_APPEND表示追加
如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容
O_CREAT若文件不存在则创建它
使用此选项时需要提供第三个参数mode,表示该文件的访问权限
文件权限由open的mode参数和当前进程的umask编码共同决定
0777 & (~0002) = 0775 111111111 111111101& 111111101
O_EXCL如果同时指定了O_CREAT,并且文件已存在,则出错处理
O_TRUNC如果文件已存在,则将其长度阶段(Truncat)为0字节
O_NONBLOCK设置文件为废阻塞状态
常见错误
1.打开文件不存在
2.以写方式打开只读文件(打开文件没有对应权限)
3.以只写方式打开目录
close函数
参数:open函数的返回值
0-->正常关闭
-1-->关闭出现错误
read函数
从打开的设备或文件中读取数据
-1-->错误
>0-->读出的字节数
=0-->文件读取完毕
fd:文件描述符
buf:数据缓冲区
count:请求读取的字节数
write函数
向打开的设备或文件中写数据
buf:需要输出的缓冲区
count:最大输出字节数
>=0-->写入文件的字节数
lseek函数
函数作用
修改文件偏移量(读写位置)
fd-->文件描述符
off_t offset-->偏移量
whence-->偏移位置
SEEK_SET-从文件头向后偏移
SEEK_CUR-从当前位置向后偏移
SEEK_END-从文件尾部向后偏移
较问价起始位置向后的偏移量
允许超过文件结尾设置偏移量,文件会因此被拓展
失败返回-1
应用
拓展文件空间
获取文件长度
返回值即为文件长度
errno
定义在头文件errno.h中
全局变量
任何标准C库函数都能对其进行修改(Linux系统函数更可以)
错误宏定义位置
第1-34个错误定义:/usr/include/asm-generic/errno-base.h
第35-133个错误定义:/usr/include/asm-generic/errno.h
是记录系统的最后一次错误代码代码是一个int型的值
每个errno值对应着以字符串表示的错误类型
当调用”某些“函数出错时,该函数会重新设置error的值
perror函数
头文件
stdio.h
函数定义
void perror(const char *s)
函数说明
用来将上一个函数发生错误的原因输出到标准设备(stderr)
参数s所指的字符串会先打印出来,后面再加上错误原因字符串
此错误原因依照全局变量error的值来决定要输出的字符串
目录操作
chdir函数
修改当前进程的路径
函数原型:int chdir(const char *path);
getcwd函数
获取当前进程工作目录
mkdir函数
作用:创建目录
注意:创建的目录需要有执行权限,否则无法进入目录
函数原型
rmdir函数
作用:删除一个空目录
函数原型:int rmdir(const char *pathname);
opendir函数
作用:打开一个目录
函数原型:DIR *opendir(const char *name);
DIR结构指针,该结构是一个内部结构,保存所打开的目录信息,作用类似于FILE结构
函数出错返回NULL
readdir函数
作用:读目录
函数原型:struct dirent *readdir(DIR *dirp);
d_type
DT_BLK-块设备
DT_CHR-字符设备
DT_DIR-目录
DT_LNK-软链接
DT_FIFO-管道
DT_REG-普通文件
DT_SOCK-套接字
DT_UNKNOWN-未知
-D_BSD_SOURCE编译时添加宏定义
closedir函数
作用:关闭目录
进程间通信
IPC方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
模式图
数据传递方式
文件、管道、信号、共享内存、消息队列、套接字、命名管道7种
常见的通信方式
1.管道(使用最简单)
2.信号(开销最小)
3.共享映射区(无血缘关系)
4.本地套接字(最稳定)
单工、半双工、全双工
管道
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。
1.其本质是一个伪文件(实为内核缓冲区)
2.由两个文件描述符引用,一个表示读端,一个表示写端
3.规定数据从管道的写端流入管道,从读端流出
原理
管道实为内核使用的环形队列机制,借助内核缓冲区(4K)实现
局限性
1.数据自己读不能自己写
2.数据一旦被读走,便不在管道中存在,不可重复读取
3.由于管道采用半双工通信方式,因此,数据只能在一个方向上流动
4.只能在有公共祖先的进程间使用管道
pipe函数
创建管道
int pipe(int pipefd[2]);\t\t成功:0;失败:-1,设置errno
返回
函数调用成功返回r/w两个文件描述符。
无需open,但需手动close。
规定
fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
使用方式/步骤
1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端;
2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
读写行为
①读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据
管道写端被全部关闭,read返回0(好像读到文件末尾)
写端没有全部关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
②写管道
管道读端全部被关闭,进程异常终止(也可以使用捕捉SIGPIPE信号,使进程不终止)
管道读端没有全部关闭
管道已满,write阻塞
管道未满,write将数据写入,并返回实际写入的字节数
管道缓冲区大小
ulimit -a查看当前系统中创建管道文件所对应的内核缓冲区大小
通常为pipe size (512bytes)8
使用fpathconf函数
管道的优劣
优点
简单,相比信号,套接字实现进程间通信,简单很多
缺点
只能单向通信,双向通信需要建立两个管道
只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决
FIFO
命名管道,不相关的进程也能交换数据
FIFO是Linux基础文件类型中的一种,但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中的一条通路。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信
创建方式
命令:mkfifo 管道名
库函数:int mkfifo(const char *pathname, mode_t mode);成功:0,失败:-1
一旦使用mkfifo创建了一个fifo,就可以使用open打开它,常见的文件I/O函数都可以用于fifo,如:close、read、write、unlink等
共享存储映射
文件实现进程间通信
fork后父子进程共享文件描述符
无血缘关系进程打开同一文件
存储映射I/O
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap函数
addr
建立映射区的首地址,由Linux内核指定,使用时直接传递NULL
length
欲创建的映射区大小
prot
映射区权限PROYT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags
标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)MAP_SHARED:会将映射区所做的操作反映到物理设备上MAP_PRIVATE:映射区所作的修改不会反映到物理设备
fd
用来建立映射区的文件描述符
offset
映射文件的偏移(4K的整数倍)
成功返回映射区首地址
失败返回MAP_FAILED宏
1.创建映射区的过程中,隐含着一次对映射文件的读操作(意味着文件必须有读权限)
2.当MAP_SHARED时,要求:映射区权限应<= 文件打开的权限(出于对映射区的保护) 当MAP_PRIVATE时,无所谓,因为mmap中的权限是对内存的限制
3.映射区的释放与文件关闭无关。只要映射区建立成功,文件可以立即关闭
4.特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!! mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的
5.munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++/--操作
6.文件偏移量必须是4k的整数倍(映射区是由内核中的MMU来创建的)
7.mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作
munmap函数
同malloc函数申请内存空间类似,mmap建立的映射区在使用结束后也应调用类似free的函数来释放
mmap父子进程通信
父子的等有血缘关系的进程之间可以通过mmap建立的映射区来完成数据通信但相应的要在创建映射区时指定相应的标志位参数flags
MAP_PRIVATE:(私有映射)父子进程各自独占映射区
MAP_SHARED:(共享映射)父子进程共享映射区
结论
父子进程共享
1.打开的文件
2.mmap建立的映射区(但必须适应MAP_SHARED)
匿名映射
解决的问题
每次创建映射区一定要依赖一个文件才能实现,通常为了建立隐射区要open一个temp文件,创建好再unlink、close,比较麻烦,可以使用匿名映射来代替
方案
借助标志位参数flags来指定
使用MAP_ANONYMOUS(或MAP_ANON)
注意文件描述符fd为-1
MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
① fd = open(\"/dev/zero\
mmap无血缘关系进程间通信
实际上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用映射区完成数据传递,由于内核空间多进程共享,因此无血缘关系的进程也可以使用mmap来完成通信只要设置相应的标志位参数flags即可(MAP_SHARED)
strace ./test 得到可执行文件执行时进行的系统调用
使用同一文件创建映射区
可多读端,多写端
目录项
线程
线程概念
什么是线程
LWP:轻量级的进程,本质仍是进程(在Linux环境下)
共享进程地址空间
拥有独立的PCB
Linux下
线程是最小的执行单位
进程是最小分配资源单位,可看成是只有一个线程的进程
内核实现
创建线程和创建进程类似-->clone
有不同的PCB,但三级页表相同
进程可以蜕变成线程
是寄存器和栈的集合
以LWP号作为分配cpu资源依据(ps -Lf 进程ID可查看)
三级映射
进程PCB-->页目录(可看成数组,首地址位于PCB中)-->页表-->物理页面-->内存单元
区别
各进程的页目录、页表、物理页面各不相同,映射不同的物理页面
线程不同,两个线程具有各自独立的PCB,但共享一个页目录,也就是共享同一个页表和物理页面,所以两个PCB共享一个地址空间
共享资源
1.文件描述符表
2.信号处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间(.text/.data/.bss/heap/共享库,除去栈)
线程间共享全局变量。
非共享资源
1.线程ID
2.处理器线程和栈指针(内核栈)
3.独立的用户栈空间
4.errno变量
5.信号屏蔽字
6.调度优先级
优缺点
提高程序并发性
开销小
数据通信方便、共享
库函数稳定性差
调试、编写困难,不支持gdb
对信号支持不好
man page安装
sudo apt-get update 更新本地软件列表
sudo apt-get install manpages-posix manpages-posix-dev
验证: man -k pthread
线程控制原语
pthread_self函数
获取进程ID, 对应进程中的getpid()函数
pthread_t pthread_self(void);
参数:无
成功:线程ID
失败:无
线程ID
pthread_t类型
线程ID是线程内部,识别标志。(两个进程间,线程ID允许相同)
不应使用全局变量获取线程ID,应使用函数
pthread_create函数
创建一个新线程, 对应于进程中的fork()函数
Linux环境下,所有线程特点,失败均直接返回错误号
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束
参数4:线程主函数执行期间所使用的参数。
pthread_exit函数
将单个线程退出
void pthread_exit(void *retval);
retval表示线程退出状态
NULL无退出值
对比
return
返回到调用者那里去
pthread_exit()
将调用该函数的线程退出
exit()
将进程退出
pthread_join函数
阻塞等待线程退出,获取线程退出状态。 对应于进程中wait()函数
thread:线程ID(注意:不是指针)
retval:存储线程结束 状态
1.如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值
2.如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常量PTHREAD_CANCELED
3.如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数
4.如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数
pthread_datach函数
实现线程分离
int pthread_detach(pthread_t thread);\t成功:0;失败:错误号
线程分离状态:1.线程与主控线程断开关系 2.线程结束自己自动释放
pthread_cancel函数
杀死(取消)线程, 对应于进程的kill函数
int pthread_cancel(pthread_t thread);\t成功:0;失败:错误号
注意:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)
取消点
是线程检查是否被取消,ing请求进行动作的一个位置。通常是一些系统调用create,open,pause... man 7 pthread可查看具备这些取消点的系统调用列表
如果线程中没有取消点,可以通过调用pthread_testcancel函数来自行设置一个取消点
终止线程方式
1.从线程主函数return,这种方法对主控线程不适用,从main函数return相当于exit
2.一个线程可以调用pthread_cancel终止同一进程中的另一个线程
3.线程可以调用pthread_exit终止自己
pthread_equal函数
比较两个线程ID是否相等
控制原语对比
fork
pthread_create
exit(10)
pthread_exit(void *)
wait(int *)
阻塞分离22: cancel -1
kill
pthread_cancel
取消点,
getpid
pthread_self
pthread_detach
分离自动清理pcb
线程属性
可在创建线程之前设置修改其默认属性
pthread_attr_t结构体
线程分离状态
线程栈空间大小
线程警戒区大小
线程栈低地址
属性初始化
pthread_attr_init函数
应先初始化线程属性,再pthread_create创建线程,最后释放资源
int pthread_attr_init(pthread_attr_t *attr); 成功:0;失败:错误号
attr:(传出参数)待初始化的结构体
pthread_attr_destory函数
销毁线程属性所占的资源
int pthread_attr_destroy(pthread_attr_t *attr); 成功:0;失败:错误号
线程的分离状态
pthread_attr_getdetachstate函数
获取线程分离状态
attr:线程属性结构体指针
detachstate:(传出参数)
PTHREAD_CREATE_DETACHED(分离)
PTHREAD _CREATE_JOINABLE(非分离)
pthread_attr_setdetachstate函数
设置线程分离状态
线程栈地址
pthread_attr_getstack函数
获取线程栈地址
attr:线程属性结构体指针
stackaddr:传出参数void**
stack:传出参数 void *
获取线程栈大小
pthread_attr_setstack函数
设置线程栈地址
线程栈大小
pthread_attr_getstacksize函数
pthread_attr_setstacksize函数
stacksize:设置线程找大小
NPTL
pthread库版本
查看
getconf GNU_LIBPTHREAD_VERSION
gcc编译指定 -lpthread选项
线程使用注意细节
退出线程应使用pthread_exit
避免僵尸线程产生
pthread_join回收线程
pthread_detach分离线程
指定分离属性,再pthread_create线程
线程共享进程地址空间
malloc和mmap内存可被其他线程释放
线程中应避免使用fork,除非马上exec
线程fork后,除调用线程外,其他线程均pthread_exit
避免线程和信号混用
0 条评论
回复 删除
下一页