Linux系统编程
(感谢其他博主的内容,有借用其他博主的图片如小林coding等)
文件IO
主要函数
-
打开和关闭文件相关函数: - open:打开文件 - creat:创建新文件 - close:关闭文件 读取和写入文件相关函数: - read:从文件中读取数据 - write:向文件中写入数据 - pread:从指定位置读取数据 - pwrite:从指定位置写入数据 - fread:从文件中读取数据到缓冲区 - fwrite:将缓冲区数据写入文件 定位文件相关函数: - lseek:设置文件偏移量 - fseek:设置文件流位置 - ftell:获取文件流位置 - rewind:重置文件流位置 fcntl:改变文件描述符属性
主要注意的就是对于文件操作时,文件指针的位置问题,可以使用定位文件相关函数来调整文件指针的位置,防止出错。
例如:
int main( )
{
/* 文件内容 123456789 */
int fd = open("/home/jemee/C_project/test/1",O_RDWR );
char buf[1024];
int res = read(fd, buf, 3);
if(res < 0)
{
perror("read");
}
/* 从第三位开始写 内容为 123123789 */
res = write(fd, "123",3);
if(res < 0)
{
perror("write");
}
return 0;
}
我们通过fd可以找到文件描述符结构体,每打开一个文件都会有一个对应的fd和一个对应的文件描述符结构题,里面存的由inode信息,可以找到操作的文件,当打开两个相同的文件是,如上图所示,会有两个文件描述符结构体指向同一个文件,但文件描述符的结构体里包含文件指针位置,所以不会造成操作时相互干扰。
阻塞非阻塞
- 阻塞IO函数
阻塞IO函数会在数据准备好之前一直阻塞等待,例如read和write函数。调用这些函数时,如果没有数据就绪,则应用程序会进入睡眠状态,并且CPU处于空闲状态。在这种情况下,无法进行其他操作,因此需要等待数据就绪后才能继续执行。
- 非阻塞IO函数
非阻塞IO函数会立即返回,而不一直等待数据准备就绪,例如fcntl函数中的O_NONBLOCK选项、ioctl函数中的FIONBIO等。使用这些函数时,如果没有数据就绪,则会立即返回一个错误码(如EAGAIN或EWOULDBLOCK),通知应用程序数据还未准备好,并且CPU可用于处理其他任务。
- IO多路复用函数
IO多路复用函数(如select、poll、epoll等)可以同时监听多个文件描述符,当其中任意一个文件描述符就绪时,就可以进行相应的操作。在使用这些函数时,它们会将所有要监听的文件描述符添加到内核中的一个等待队列中,并且将应用程序进程挂起,等待某个文件描述符就绪。这种模型可以充分利用CPU资源。
打开文件或者读写文件时设置阻塞和非阻塞模式。
- 打开文件
打开文件时可以指定O_NONBLOCK标志,让文件以非阻塞模式打开。例如:
int fd = open("file.txt", O_RDONLY | O_NONBLOCK);
这样打开的文件描述符fd就是非阻塞模式的了。
- 读写文件
使用read、write等函数时,也可以将文件描述符设置为非阻塞模式,例如:
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
这样,对于该文件描述符执行的读写操作都将是非阻塞的。
需要注意的是,对于某些文件IO函数(如lseek),无法通过参数来控制它们是否阻塞。在这种情况下,只能通过将文件描述符设置为非阻塞模式来实现非阻塞IO操作。
标准IO
主要函数
文件打开和关闭函数
FILE *fopen(const char *filename, const char *mode):打开文件,并返回文件指针。
int fclose(FILE *stream):关闭文件。
读取和写入文件数据的函数
getline(char **lineptr, size_t *n, FILE *stream); 从文件流读一整行
size_t fread(void *ptr, size_t size, size_t count, FILE *stream):从文件中读取数据到缓冲区。
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream):将缓冲区数据写入文件。
定位文件位置和控制文件流的函数
int fseek(FILE *stream, long offset, int origin):设置文件流位置。
long ftell(FILE *stream):获取文件流位置。
void rewind(FILE *stream):重置文件流位置。
int fflush(FILE *stream):将缓冲区的数据写入文件。
int setvbuf(FILE *stream, char *buffer, int mode, size_t size):设置文件缓冲区。
需要注意的是,标准IO库并不支持非阻塞IO操作,因此在某些情况下,如嵌入式系统或者网络编程等需要考虑到实时性、高性能的场景下,应该使用Linux文件IO。
这些函数都是基于系统调用(文件IO)来实现的。
与文件IO的区别
标准IO和文件IO是两种不同的I/O操作方式。标准IO是C语言标准库提供的一组文件I/O操作函数,而文件IO是Linux系统提供的一组文件I/O操作API。
标准IO与文件IO之间的关系可以用以下几点来进行说明:
- 标准IO包含了文件IO
标准IO中的文件I/O操作函数(如fopen、fclose、fread、fwrite、fprintf等)都是基于文件IO实现的。在调用这些函数时,它们会自动使用Linux文件IO函数来执行实际的读写操作,从而实现对文件的访问。
- 标准IO支持缓存机制
与文件IO不同的是,标准IO库在进行文件读写时会对数据进行缓存,从而提高读写性能。标准IO库维护了三种类型的缓冲区:全缓冲、行缓冲和不带缓冲。应用程序可以使用setbuf或setvbuf函数来控制缓冲区的类型和大小。
- 标准IO不支持非阻塞IO
标准IO库不支持非阻塞IO操作,因此在某些情况下,如嵌入式系统或者网络编程等需要考虑到实时性、高性能的场景下,应该使用Linux文件IO。(fopen返回结构体地址存放在堆上,有fclose互逆操作,一般有互逆操作的都存放在堆上)(文件IO直接与内核打交道,标准IO先于库缓存打交道再与内核打交道,文件IO*每次操作都要写入内核,而标准IO达到缓存量开始写一次,一个响应速度快,一个吞吐量大)
注意点
1、三种缓存机制:行缓存,全缓存和无缓存
**行缓冲:**换行,遇到新行符(\n)时候刷新,满了的时候刷新,强制刷新(标准输出是这样的,因为是终端设备)
读: fgets, gets, printf, fprintf.spintf.
写: fputs, puts.scanf
*全缓冲:满了的时候刷新,强制刷新(默认,只要不是终端设备) 文件
读: fread
写: fwrite.
无缓冲:如stderr,需要立即输出的内容
stderr 错误输出
只要用户调这个函数,就会将其内容写到内核中。
!!!由于缓存机制在,在进行进程fork时需要先对文件进行强制刷新,不然会对缓存区进行复制,照成意想不到的错误。
2、文件指针位置,与文件IO类似。
文件系统
操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统,在 Linux 文件系统中,用户空间、系统调用、虚拟机文件系统、缓存、文件系统以及存储之间的关系如下图:
文件描述符相关函数:
- dup:复制文件描述符
- dup2:复制文件描述符并指定新的文件描述符,整个过程为原子操作
- fcntl:改变文件描述符属性
文件状态相关函数:
- stat:获取文件信息
- fstat:获取文件描述符关联文件信息
- chmod:修改文件权限
- chown:修改文件所有者和所属组
- utime:修改文件访问和修改时间
文件类型:
16位的位图表示 7种文件类型 b c d - l s p 不错的-老色批
• b 块设备文件
• c 字符设备文件
• d 目录
o 常规文件
• l 符号链接文件
• s socket文件
• p 匿名管道文件(不可见)
分析目录
-
int glob(const char *pattern, int flags, int errfunc(const char *epath, int eerrno), glob_t pglob);
glob函数搜索匹配 函数pattern中的参数,如/*是匹配根文件下的所有文件(不包括隐藏文件,要找的隐藏文件需要从新匹配),然后会将匹配出的结果存放到 pglob,即第4个参数中,第二个参数能选择匹配模式,如是否排序,或者在函数第二次调用时,是否将匹配的内容追加到pglob中,等,第3个参数是查看错误信息用,一般置为NULL;
-
globfree:
时间戳
- time() 从kernel中取出时间戳(以秒为单位)
- gntime() 将时间戳转换为struct_tm 格林威治时间
- localtime() 将时间戳转换为struct_tm 本地时间
- mktime() jaing struct_tm结构体转换为时间戳,还可以检查是否溢出
- strftime(); 格式化时间字符串
目录IO(属于文件IO)
常用函数
目录操作相关函数:
- opendir:打开目录
- readdir:读取目录项
- closedir:关闭目录
软连接和硬链接
硬链接是多个目录项中的「索引节点」指向一个文件,也就是指向同一个 inode,但是 inode 是不可能跨越文件系统的,每个文件系统都有各自的 inode 数据结构和列表,所以硬链接是不可用于跨文件系统的。由于多个目录项都是指向一个 inode,那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候,实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
多路IO转接
监控文件描述的
select()
poll()
epoll()
文件每次被操作之后文件描述符结构体会保存把当前文件的状态保存(通过位图进行判断当前状态),可提供上层函数监控。
poll监视的文件描述符是通过内核的file_operations中的poll函数实现的。poll函数是Linux内核提供的一种系统调用,用于监视文件描述符上的事件。当文件描述符上有事件发生时,poll函数会返回一个可读/可写/异常事件的集合,进程可以根据这个集合来进行相应的处理。在内核中,每个打开的文件都有一个file结构体,这个结构体包含了一些函数指针,其中包括poll函数指针,用于处理文件描述符上的事件。当进程调用poll函数时,内核会根据文件描述符找到对应的file结构体,并调用其中的poll函数来处理事件。
位运算
将操作数中第n位置1,其他位不变:num= num | 1 << n;
将操作数中第n位清0,
其他位不变: num=num&~(1<<n);
测试第n位: if(num & 1 << n)
从一个指定宽度的数中取出其中的某几位: ? ?
文件锁
- fcntl()
- lockf()
- flock()
存储映射IO
把内存中的内容 或者 某一个文件的内容 映射到当前进程空间中来
-
mmap(void *addr,size_t length,int prot,int flags,int fd,odd_t offset);
-
- 匿名映射可以实现malloc的功能
-
munmap(void *addr,size_t length)
进程线程
进程
当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」。
进程状态
- 运行状态(Runing):该时刻进程占用 CPU;
- 就绪状态(Ready):可运行,但因为其他进程正在运行而暂停停止;
- 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;
当然,进程另外两个基本状态:
-
创建状态(new):进程正在被创建时的状态;
-
结束状态(Exit):进程正在从系统中消失时的状态;
说明一下进程的状态变迁:
- NULL -> 创建状态:一个新进程被创建时的第一个状态;
- 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
- 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
- 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
- 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
- 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
- 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
另外,还有一个状态叫挂起状态,它表示进程没有占有物理内存空间。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。
挂起状态可以分为两种:
- 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
- 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行
进程的调度
通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:
- 将所有处于就绪状态的进程链在一起,称为就绪队列;
- 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;
- 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。
那么,就绪队列和阻塞队列链表的组织形式如下图:
就绪队列和 除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。
一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。
进程的产生(没有关系和辈分关系)
fork(); 复制当前进程 调度器的调度策略来决定父子进程哪个先运行,共用的资源父进程需要改动,父进程就会再拷贝一份,子进程也是这样,两个都不改动,就共用一块资源。
fork后父子进程的区别:
1) 返回值
2) Pid ppid 不同
3) 未决信号及文件锁不继承
4) 资源利用量清0
Init进程是所有进程的祖先进程 为1号进程
fork之前要加上刷新流,防止父进程的缓存影响子进程
进程终止的方式:
正常终止
o 从main函数返回
o 调用exit
o 调用_exit或者_Exit
o 最后一个线程从其启动例程返回
o 最后一个线程调用pthread_exit
异常终止
o 调用abort
o 接到一个信号并终止
o 最后一个线程对其取消请求作出响应
进程的消亡及释放资源
wait() 死等
waitpid()
exec函数族
exec 用一个进程映像替换 当前进程映像所以永远不会有返回情况
extern char **environ
execl
execlp
execle
execv
execvpa
在exec函数族之前要使用缓冲区刷新函数 原因和 fork相似
进程的上下文切换
进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:
进程间通信方式
管道(有名、无名)
共享内存
信号
信号量
socket
队列
进程会计
acct();
进程时间
times();
守护进程
脱离控制终端 ppid为1 pid = pgid = sid
会话 session sid
终端
setsid( ) ;
getpgrp() ;
getpgid();
setpgid() ;
线程
在早期的操作系统中都是以进程作为独立运行的基本单位,直到后面,计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程,可以理解为线程是进程当中的一条执行流程。
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程与进程的比较
线程与进程的比较如下:
- 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销;
对于,线程相比进程能减少开销,体现在:
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
所以,线程比进程不管是时间效率,还是空间效率都要高。
优缺点
用户线程的优点:
- 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
- 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;
用户线程的缺点:
- 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。
- 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。
- 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;
线程的创建与销毁
需要考虑是否可重入
pthread_creat()
pthread_cancel() 线程取消
取消有2种状态:允许和不允许
允许取消又分为:异步cancel,推迟cancel(默认)->推迟至cancel点在响应取消
Cancel点:POSIX定义的cancel点,都是可能引发阻塞的系统调用pthread_setcancelstate():设置是否允许取消
pthread_setcanceltype():设置取消方式
pthread_testcancel() : 什么都不做,就是一个取消点
pthread_exit();//退出一个线程,类似于进程的exit()函数。
pthread_join() 主线程阻塞等待子线程退出,并获取子线程退出状态码。类似于进程的wait()函数
pthread_detach();//使线程处于分离状态,也就是说该线程执行完后自动释放所有资源包括回收PCB。
主线程可以通道 pthread_cancel 主动终止子线程,但是子线程中可能还有未被释放的资源,比如malloc开辟的空间。如果不清理,很有可能会造成内存泄漏。为了避免这种情况,存在线程清理函数 pthread_cleanup_push 和 pthread_cleanup_pop 。两者是宏函数,以”{“结尾,因此必须是成对存在的,否则无法编译过。
相当于挂钩子函数
pthread_cleanup_push
pthread_cleanup_pop
void* pthread_cleanup(void* args){
printf("线程清理函数被调用了\n");
}
void* pthread_run(void* args)
{
pthread_cleanup_push(pthread_cleanup, NULL);
pthread_exit((void*)1); // 子线程主动退出
pthread_cleanup_pop(0); // 这里的参数要为0,否则回调函数会被重复调用
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, pthread_run, NULL);
sleep(1);
pthread_join(tid, NULL);
return 0;
}
线程同步
互斥量: pthread_ mutex_ t;
pthread_ mutex_ init() ;
pthread_ mutex_ destroy() ;
pthread_ mutex_ Lock() ;
pthread_ mutex_ tryLock() ;
pthread_ mutex_ unLock()
条件变量
pthread_cond_t
pthread_cond_init()
pthread_cond_destory()
pthread_cond_wait() 等通知 + 抢锁 ///等待之前一定要加锁,防止在判断之前条件被改变,导致没有唤醒信号
pthread_cond_broadcast() 广播给所有线程
pthread_cond_signal() 通知任意一个线程
条件变量可以解决 互斥量进行盲等的问题 即实现了通知法,通知互斥量什么时候上锁
信号量
通过互斥量与条件变量的配合我们可以实现信号量 信号量像是一个激活函数 当这个变量超过阈值时 将会触发条件变量给互斥量上锁
调度算法
01 先来先服务调度算法
02 最短作业优先调度算法
最短作业优先(*Shortest Job First, SJF*)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
03 高响应比优先调度算法
每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,「响应比优先级」的计算公式:
04 时间片轮转调度算法
最古老、最简单、最公平且使用最广的算法就是时间片轮转(*Round Robin, RR*)调度算法。
05 最高优先级调度算法
前面的「时间片轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(*Highest Priority First,HPF*)调度算法。
06 多级反馈队列调度算法
多级反馈队列(*Multilevel Feedback Queue*)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。
信号
1、 信号的概念
信号是软件中断,信号的响应依赖于中断
2、 signal()
signal(int signum, void (*func)(int)))(int);
信号会打断阻塞的系统调用
3、 信号的不可靠
一个信号在处理的同时又来了一个相同的信号,就有可能破坏信号调用前的代码现场,处理函数要遵循快出
4、 可重入函数
第一次调用还没有结束,又发生了第二次调用,不会发生错误叫可重入函数
所有的系统调用都是可重入的,一部分库函数也是可重入的,比如memcpy
函数一般对静态变量和全局变量直接操作时不可重入
5、 信号的响应过程
信号从收到到响应有一个不可避免的延时
思考:如何忽略掉一个信号 (某一个信号的mask位置0)
标准信号为什么要丢失(执行一个信号时会把mask置0,此时再来信号也不会响应)
信号的响应有没有严格顺序(有的信号更紧急)
6、 信号的常用函数
kill() 不是用来杀死进程的,是用来发送信号的,只不过大多数信号有杀死进程的作用
pause 人为制造的阻塞系统调用 等待一个信号来打断它
abort
system()
**sleep()**在某些平台,sleep()是使用alarm+pause封装的,而程序中出现多于1个的alarmalarm将失效
signal()
sigaction() 用来替换signal() 还可以指定信号的来源以选择是否响应
7、 信号集
sigset_t 信号集类型
sigemptyset() 将一个信号集置为空集
sigfillset() 将一个信号集置为全集
sigaddset() 将一个信号加入信号集
sigdelset() 将一个信号移除信号集
sigismember()
信号屏蔽字/pending集的处理
我们无法控制信号何时到来,但可以选择如何响应它,例如:
struct sigaction sa;
sa.sa_handler = daemon_exit;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGQUIT);//屏蔽SIGQUIT、SIGTERM
sigaddset(&sa.sa_mask, SIGTERM);
sigaction(SIGTERM, &sa, NULL); // 对指定信号处理
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
system()
**sleep()**在某些平台,sleep()是使用alarm+pause封装的,而程序中出现多于1个的alarmalarm将失效
signal()
sigaction() 用来替换signal() 还可以指定信号的来源以选择是否响应
7、 信号集
sigset_t 信号集类型
sigemptyset() 将一个信号集置为空集
sigfillset() 将一个信号集置为全集
sigaddset() 将一个信号加入信号集
sigdelset() 将一个信号移除信号集
sigismember()
信号屏蔽字/pending集的处理
我们无法控制信号何时到来,但可以选择如何响应它,例如:
struct sigaction sa;
sa.sa_handler = daemon_exit;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGQUIT);//屏蔽SIGQUIT、SIGTERM
sigaddset(&sa.sa_mask, SIGTERM);
sigaction(SIGTERM, &sa, NULL); // 对指定信号处理
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);