Linux system programing 01

Linux系统编程

错误码

-1
Linux 绝大部分系统调用异常都会返回此数值,并设置错误掩码

EAGAIN or EWOULDBLOCK
使用fcntl(...), ioctl(...)描述符属性掩码被设置为nonblocking (O_NONBLOCK),当可读数据低于内核可读水位线时,对应系统调用会返回-1,且设置错误码为此项.例如读请求遇到读buff为空,写请求遇到写buff满. 此时用户层调用不会阻塞.

EBADF
传入fd 文件描述符为非法值 不在(0-65535),或此时该文件描述符已无指向(不在描述符列表中) 或者传入描述符无对应操作权限[int open(...) 创建只写  O_WRONLY 描述符并将此描述符做read(...)读操作].

EFAULT
传入指针指向的地址空间非法(传入的buffer 地址)

EINTR
系统调用被信号中断,获取此错误时,一般作continue/break 跳出到下次流程继续上次作业

EINVAL
传入的描述无无对应权限[int open(...) 创建只写  O_WRONLY 描述符并将此描述符做read(...)读操作]; 或者此文件使用 O_DIRECT 设置了属性(在read(...)时), 写入地址未对齐

EIO
I/O error.设备故障或者驱动故障。

EISDIR 传入的描述符指向一个目录,不是一个IO流或者文件描述符

ENOTDIR 传入的描述符不是一个目录,不是一个IO流或者文件描述符

底层文件操作(不带缓冲的文件)、基本操作、printf、fprintf、sprintf

  • fscanf
  • scanf_s (msvc)
  • printf
  • printf_s (msvc)
  • fprintf
  • sprintf
Linux system I/O calls
  • socket
  • ssize_t read(int fd, void *buf, size_t count);
  • write(int fd, void *buf, size_t count)
  • close
  • sync
  • fsync

聚簇IO

  • readv
  • writev

进程

进程的基本概念、fork、exec、进程的五种状态切换、exit、shell的实现、守护进程

PCB(进程控制块),程序段,数据段三部分构成了进程实体(又叫进程映像)。

程序段:描述进程本身要完成的功能;

数据段:程序加工的对象和场所;

进程控制块:内存中存储,记录进程生存周期内状态变化的存储区域。不同操作系统有不同的进程控制块格式和信息,但基本包括进程标识符,当前状态,现场保护区,存储指针,占用资源表以及进程优先级等信息。它是进程存在的唯一标志。

进程

程序执行时的一个实例,是资源分配的最小单位.PCB是进程存在的唯一标志。

进程五种状态(按生命周期开始-结束)

创建态:进程正在被创建,系统为其初始化PCB,分配资源。

就绪态:已经具备运行条件,但由于没有空闲CPU,而暂时不能运行.没有获得CPU

运行态:占有CPU,并在CPU上执行中.

阻塞态: 因等待某一事件而不能运行.

终止态: 进程正在从系统中撤销,回收进程持有的资源,撤销其PCB.

进程间通信

进程通信主要有三种方式

(1) 共享内存

(2) 消息传递

(3)管道通信:管道只能采用半双工通信.若要实现同时双向通信则需要设置两个管道.各进程要互斥地访问管道.如果没写满,就不会允许读.如果没读空,就不允许写.管道本质是一块在内存中开辟的固定大小的缓冲区.

线程

程序执行的最小单位

fork()

int pid fork(); fork() 成功后会返回一个从系统获得的进程ID.一个进程子拥有独立的PCB,有和父进程一样的代码段、数据段.父、子进程靠进程ID(PID)区分.
getpid() 返回当前进程PID getppid() 返回父进程ID

读时共享,写时复制-[父进程在操作这个物理内存块时(比如修改变量的值),再复制该部分的实际物理内存到子进程中.这就是写时复制。]

vfork()
创建一个与父进程[共享]数据段的子进程.

clone()

父子相同处: 全局变量、.数据段、.代码段、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...

父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集

exec函数族

系统调用,exec函数族就提供了一个在进程中启动另一个程序执行的方法.

exec l le lp exec v ve vp

这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用

exec() 函数 [系统调用]

exit()就是退出,传入的参数是程序退出时的状态码,0表示正常退出,其他表示非正常退出,一般都用-1或者1.

exit()在结束调用它的进程之前,要进行如下步骤:

1.调用atexit()注册的函数(出口函数);按ATEXIT注册时相反的顺序调用所有由它注册的函数,这使得我们可以指定在程序终止时执行自己的清理动作.例如,保存程序状态信息于某个文件,解开对共享数据库上的锁等.

2.cleanup();关闭所有打开的流,这将导致写所有被缓冲的输出,删除用TMPFILE函数建立的所有临时文件.

3.最后调用_exit()函数终止进程。

shell实现

父进程负责读取标准输入流 main(args[] : char* ) 中的命令

fork() 出的子进程负责系统调用.

守护进程

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以root权限运行,因为它们要使用特殊的端口或访问某些特殊的资源.如msqld进程 httpd进程

一个守护进程的父进程是init进程.

编写守护进程的一般步骤步骤:

(1) 在父进程中执行fork并exit推出;

(2) 在子进程中调用setsid函数创建新的会话;

(3) 在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;

(4) 在子进程中调用umask函数,设置进程的umask为0;

(5) 在子进程中关闭任何不需要的文件描述符

(6) 处理SIGCHLD信号

僵尸进程

父进先比子进程先挂了,子进程将成为僵尸进程从而占用系统资源


IPC

IPC基本概念、管道、信号(进程间异步通信机制)、消息队列、内存共享、信号量(信号灯)

什么是IPC

linux下的多个进程间的通信机制叫做IPC,它是多个进程之间相互沟通的一种方法.

linux下多种进程间通信的方法:

半双工管道、命名管道、消息队列、信号、信号量、共享内存、内存映射文件,套接字等等.

管道

半双工

只能和具有亲缘关系的进程通信

管道是由内核管理的一个缓冲区,只能在本地计算机使用.

命名管道

允许没有亲缘关系的进程间通信

管道与命名管道区别

管道只能由程序创建只能和有亲缘关系的进程通信,而命名管道可以用 mkfifo _fifoName_命令提前创建好,进程只要调用文件操作函数即在命名管道中读写数据.

二者默认读写模式是阻塞的,若想设为为阻塞读写,则只需在调用 open()函数时将参数设置为非阻塞 O_NONBLOCK .

信号

信号是进程间通信机制中唯一的异步通信机制;信号机制是进程间传递消息的一种机制,是异步进程中通信的一种方式.
处于执行状态的进程,信号会被优先处理.

进程间异步通信机制

如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递个它;

如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞取消时才被传递给进程。

有两种信号不能被忽略,分别是SIGKILL和SIGSTOP。因为它们向内核和超级用户提供了进程终止和停止的可靠方法.

消息队列

消息的链表,存放在内核中,一个队列由一个队列 ID 标识.

消息队列中的消息有特定的格式和特定的优先级(有不同类型的消息)

消息队列独立于发送和接收进程。进程终止,消息队列及内容并不会删除

消息队列可实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息类型读取

msgget()

创建一个新队列或打开一个存在的队列;

msgsnd()

向队列末端添加一条新消息;

msgrcv()

从队列中取消息

共享内存

两个或多个进程共享一个给定的存储区

共享内存是最快的一种 IPC

信号量 + 共享内存通常结合在一起使用,信号量用来同步对共享内存的访问(无锁化编程)

信号量

信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。

信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号) p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行 V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1

简单理解就是P相当于申请资源,V相当于释放资源

多线程同步四种方式

互斥锁(互斥量) 读写锁 条件变量 信号量

死锁

进程死锁的原因

1.进程竞争.

2.进程推进顺序非法.

形成死锁的四个必要条件

互斥、请求保持、不可剥夺、环路

死锁的处理

鸵鸟策略、预防策略、避免策略、检测与解除死锁


IO多路复用

IO多路复用基本概念

多路IO流监控.

IO多路复用的作用就是感知IO(可读可写是否出错)是否就绪.这一过程是非阻塞的.

为什么使用IO多路复用?解决了什么问题?

多路IO复用是为了解决通过单个线程去记录追踪每一个文件描述符(IO流)的状态并同时管理多个IO流.

IO多路服用模型只是做文件见描述符状态的确认,并不关系描述符数据是否否正在读取,通过这一机制解决了如果使用多线程和多进程监控多个文件描述符未就绪是造成的堵塞的情况.

能用多进程代替吗?

不能.因为开销大(撤销,上下文切换)且进程数量有限(系统可分配资源有限).与多进程和多线程技术相比,I/O 多路复用技术的最大优势是系统开销小,系统不必创建进程 / 线程,也不必维护这些进程 / 线程,从而大大减小了系统的开销。

能用多线程代替吗?

不能.IO操作可能会造成堵塞.若改为循环非阻塞状态下的操作,则会无法感知IO(描述符)状态.需要花费更多的CPU时间,效率不高.

Linux socket编程

socket基本操作

select

select 1024限制

Linux:

  #define __FD_SETSIZE    1024

linux 内核限定了FD_SET 这一用于存储文件描述符的位图的大小就是1024.

1024是linux 约定的限制大小,对于大于等于1024的socket select 仍然支持注册进位图,但会出现不可预知的错误比如越界、数据覆盖等.

Linux下的解决办法

1.编译内核
2.自定义FD_SET 不适用系统内核提供的位图.

Windows:

直接在自己引入并修改Windows下的FD_SIZE宏定义即可

Windows下的FD_SET是由数组实现的.

unix(macos)

在文件中引入 系统指定的两个宏中的其中一个即可突破1024限制

// 在select 实现文件中可以找到.
#define _USE_C_SOURCE_UMLIMIT
//或
#define _USE_......_UMLIMIT //记不清了

select 性能热点

core_sys_select ()

这个函数主要功能是在实现真正的select功能前,准备好 fd_set ,即从用户空间将所需的三类 fd_set 复制到内核空间。从下面的代码中你会看到对于每次的 select系统调用,都需要从用户空间将所需的三类 fd_set 复制到内核空间,这里存在性能上的损耗。

1.轮询

拷贝到内空间需要遍历一次(监控描述符状态,修改三大集合),拷到用户空间(遍历集合,取出)

2.每次调用select都做三大集合的拷贝 用户空间->内核空间

3.每次select 返回后都会将以前加入但无事件发生的文件描述符清空.所以在下一次select开始前还需要将已接入的socket重新加入到集合中.

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个 参数。

(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)

解决办法

对于第3.

可以做一个三大集合的备份,让select在副本中操作即可.下次直接恢复备份即可.位图置位操作性能消耗高于按矢访问操作.

epoll

Epoll 的连接上线取决于 file-max.conf 配置文件中的软限制值、硬限制值和物理内存的大小.

每个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中注册进来的事件.这些事件都会挂载在红黑树中.添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

epoll高效的原因

不重复传递socket句柄给内核,通过 epoll_ctl()内核中红黑树存储要监控的句柄,当有事件发生时,epoll 不会遍历所有已注册的事件集合,而是从红黑树中找到该事件句柄,并通过双链表存储准备就绪的事件,epoll只将绪事件列表中的事件逐一处理即可.

水平触发LT

有事件发生,只要该事件未处理或未处理完(如数据未读完)就会再次通知(再次加入到就绪事件链表中)下次epoll_wait()时将会再次返回.优点:不会丢数据.缺点:只要未处理完就绪链表不为空就会一直通知.占用资源.

LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。

边缘触发[ET]

事件发生就通知,只通知一次,未处理完不在通知.只有事件发生变化后才会再次发生通知(关注边际变化),比如一个socket由读状态转为写状态,此时才会将该事件再次加入到就绪链表中.优点:只通知一次,不会频繁唤醒进程,资源占用少.缺点:未处理时间若不及时处理会出现数据丢失.

select与Epoll区别

(1)select,poll 只要读写事件发生就会不断轮询所有 fd 集合.而 epoll 其实也需要调用 epoll_wait 不断轮询的是就绪链表,是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。

(2)select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并对该集合进行遍历找出有读写事件的socket,而 epoll 在接受连接后只进行一次拷贝,即将新来连接注册进内核中用红黑树索引的集合中.