环境高级编程

文章目录

  • 文件I/O
    • 文件描述符
    • 文件共享
    • 原子操作
  • 文件和目录
    • 函数umask
    • 函数chown
  • 标准I/O库
    • 缓存
    • 打开流
    • 系统数据信息和文件
  • 进程环境
    • 进程终止
  • 环境表
  • C程序的存储空间布局
    • 共享库
  • 存储空间的分配
    • 环境变量
    • 函数setjmp和longjmp
    • 函数getrlimit和setrlimit
  • 进程控制
    • 函数wait和waitpid
    • 函数waitid
    • 函数wait3和wait4
    • 函数exec
    • 进程会计
    • 用户标识
    • 进程调度
    • 进程时间
  • 进程关系
    • 终端登入
    • 网络登录
    • 进程组
    • 会话
    • 控制终端
    • 函数tcgetpgrp、tcsetgrp、tcgetsid
    • 作业控制
    • shell执行程序
    • 孤儿进程组
  • 信号
    • 信号概念
    • sinal函数
    • 不可靠的信号
    • 中断的系统调用
    • 可冲入函数
    • SIGCLD语义
    • 可靠信号术语和语义
    • kill和raise函数
    • alarm和pause函数
    • 信号集
    • 函数sigprocmask
    • sigaction函数
    • sigsetjmp和siglongjmp函数
  • 线程
    • 线程标识
    • 线程创建
    • 线程终止
    • 线程同步
      • 互斥量
      • 避免死锁
      • 函数pthread_mutex_timedlock
      • 读写锁
      • 带超时的读写锁
      • 条件变量
      • 自旋锁
      • 屏障
  • 线程控制
    • 线程限制
    • 同步属性
      • 互斥量属性
      • 读写锁属性
      • 条件变量属性
      • 屏障属性
    • 重入
    • 线程特定数据
    • 取消选项
    • 线程和信号
    • 线程和fork
    • 线程和I/O
  • 守护进程
    • 守护进程的特征
  • 高级I/O
    • 非阻塞I/O
    • 记录锁
    • I/O多路转接(I/O 复用)
      • 三组 I/O 复用函数的比较
    • POSIX 异步 I/O
    • 函数 readv 和 writev
    • 存储映射 I/O
  • 进程间通信
    • 管道
    • 函数 popen 和 pclose
    • 协同进程
    • FIFO
    • XSI IPC
    • 消息队列
    • 信号量
    • 共享存储
    • POSIX 信号量
    • 几种 IPC 的应用
  • 网络IPC:套接字
    • 1. 网络 IPC
    • 2. 套接字描述符
      • 网络字节序
      • lisen
      • accept
    • 数据传输
    • 带外数据

文件I/O

UNIX 5个函数:open、read、write、lseek、close

不带缓冲:每个read和write都调用内核中的一个系统调用
环境高级编程_第1张图片

文件描述符

对于内核,所有的文件描述符都是通过内核引用的,当打开一个文件或创建一个新文件,内核像应用进程返回一个文件描述符

UNIX系统shell把

  • 文件描述符0与进程的标准输入关联

  • 文件描述符1与标准输出相关联

  • 文件描述符2与标准错误相关联

函数open和openat

#include 
int open(const char* pathname,int oflag,..../*mode_t mode*/); 
//返回值:成功的话返回fd,出错返回-1
//pathname打开的文件名字

create函数创建一个新的文件

#include
int create(const char *pathname, mode_t mode);
//返回值:如果成功返回为只写打开的fd,若出错则返回-1

close函数关闭一个打开的文件

#include 
int close (int filedes);

lseek函数

每一个打开的文件都有一个与其相关联的“当前文件偏移量”,它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都是从文件开始处计算的字节数,并使偏移量增加所读写的字节数。

可以调用lseek显式的为一个打开的文件设置其偏移量。

#include 
off_t lseek(int filedes,off_t offset,int whence);
//返回值:若返回则返回新的文件偏移量,若出错则返回-1;

whence的值通常为如下值:

  • SEEK_SET:偏移量设置为距文件开始处offset个字节

  • SEEK_CUR:偏移量设置为其当前值加offset,offset可为正或者负值

  • SEEK_END:偏移量设置为文件长度加offset,offset可为正或者负值

read函数从打开文件中读数据

#include
ssize_t read(int filedes,void *buf,size_t nbytes);

如果read成功,则返回读到的字节数,如果已经到达结尾,则返回0。

write函数向打开的文件写数据。

#include
ssize_t write(int filedes,const char* buf, size_t nbytes);
//返回值:若成功则返回已写的字节数,若出错则返回-1;

文件共享

UNIX系统支持在不同进程间共享打开的文件。

内核使用三种数据结构表示打开的文件,他们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

1.每个进程在进程表项中都有一个记录项,记录项中包含有一张打开文件描述符表

  • 文件描述符标志

  • 指向一个文件表项的指针。

img

2.内核为所有打开文件维持一张文件表。每个文件表项包含:

  • 文件状态标志(读、写。添写、同步和非阻塞等)。

  • 当前文件偏移量。

  • 指向该文件V节点表项的指针。

3.每个打开文件或者设备都有一个V节点结构。V节点包含了文件类型和对此文件进行各种操作的函数的指针

对于大多数文件,V节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘读入内存的,所以所有关于文件的信息都是快速可供使用的。例如,i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件实际数据块在磁盘上所在位置的指针等。

Linux没有V节点,而是使用了通用i节点结构。显然两种实现有所不同,但是在概念上

该进程有两个不同的打开文件:一个文件打开为标准输入(文件描述符为0),另一个打开为标准不=输出(文件描述符为1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cj5sP1qE-1596696659135)(/home/xiaohu/.config/Typora/typora-user-images/image-20200717211041981.png)]

如果两个独立进程各自打开了同一个文件,打开该文件的每一个进程都得到一个文件表项,但对一个给定为文件只有一个V节点表项。每个进程都有自己的文件表项的理由是:这种安排使每个进程都有他自己的对该文件的当前偏移量。

img

原子操作

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位

pread函数和pwrite函数

允许原子性的定位搜索(lseek)和执行I/O。

#include 
ssize_t pread(int filedes, void *buf, sizt_t nbytes, off_t offset);
//返回值:读到的字节数。若已到文件结尾则返回0,若出错则返回-1;

调用pread相当于调用lseek和read,但是pread又与这种顺序调用有下列重要区别:

  • 调用pread时,无法中断其定位和读操作。

  • 不更新文件指针;

一般而言,原子操作指的是有多步组成的操作,如果该操作原子的执行,则要么执行完所有的步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

dup和dup2函数

复制一个现存的文件描述符:

#include 
int dup(int filedes);
int dup2(int filedes,int filedes2);
//返回值:若成功则返回新的文件描述符,若出错则返回-1;

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数指定新描述符的数值。如果filedes已经打开,则现将其关闭。如若filedes等于filedes,dup2返回filedes2,而不关闭它。

这些函数返回的新文件描述符与参数filedes共享一个文件表项(内核)。

因为两个描述符指向同一文件表项,所以他们共享同一文件状态标志(读、写、添加等)以及同一当前文件偏移量。

每个文件描述符都有他自己的一套文件描述符标志。

img

sync、fsync和fdatasync函数

UNIX系统实现在内核中设有高速缓存或页高素缓存,大多数磁盘I/O都通过缓冲区进行

向内核写入数据,内核通常将数据复制到缓存区,然后排入队列,晚些再写入磁盘

为了保证磁盘上的实际文件系统和与缓冲区高速缓冲区中的内容一致,UNIX系统提供了三个函数

#include 
int fsync(int filedes);
int fdatasync(int filedes);
返回值:若成功则返回0,失败则返回-1;
void sync();

fcntl函数

fcntl函数可以改变已打开文件的性质。

#include 
int fcntl(int filedes, int cmd, ... /*int arg*/);
返回值:若成功则依赖于cmd,若出错则返回-1;

ioctl函数

文件和目录

《UNIX环境高级编程》目录:https://blog.csdn.net/isunbin/article/details/83547474

#include 
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf)
int lstat(const char *path, struct stat *buf);
int fstat(int fd, const char *path, struct stat *buf, int flag);

一但给出pathname

  • stat函数返回与此命名文件有关的信息结构,

  • fstat函数获得已在描述符fd上打开文件的有关信息,  ll

  • lstat函数类似于stat,但是当命名的文件是一个符号链接时,lstat返回符号链接的有关信息,而不是由该符号链接引用的文件的信息,

  • fstatat函数为一个相对于当前打开目录(由fd指向)的路径名返回文件统计信息

函数umask

#include 
mode_t umask(mode_t mask);

在进程创建一个新的文件或目录时,如调用open函数创建一个新文件,新文件的实际存取权限是mode与umask按照 mode&~umask运算以后的结果。umask函数用来修改进程的umask。

函数chown

#include 
int chown(const char *pathname, uid_t owner, gid_t group);
//若成功,返回0;若出错,返回-1
  • pathname:要更改的文件名
  • owner:拥有者 uid
  • gropu:所属组 gid

标准I/O库

当我们打开或创建了一个文件,我们说我们有一个流和该文件关联。

  • freopen会清除流的orientation;
  • fwide用来设置流的orientation。

缓存

缓存(buffering)的作用是为了尽可能少地调用read和write系统调用。

标准IO库提供三种类型的buffering:

  • 完全缓存(Fully buffered):在这种缓存机制中,实际的IO操作发生在缓存被写满时。正在写入硬盘的文件被完全缓存在buffer中。缓存空间往往在第一次IO操作时通过调用malloc函数获取;

  • 行缓存(Line buffered):在这种缓存机制中,实际的IO操作发生在新的一行字符被读入或者输出时,所以允许每一次只输出一个字符。行缓存有两点需要注意:buffer的大小是固定的,所以即使当前行没有读入或输出结束,依然可能发生实际的IO,当buffer被写满时;一旦有输入(从无缓存流或者行缓存流中输入)发生,所以已在buffer中缓存的输出流都会被立刻输出(flush)。

    flush:标准IO缓存中内容立刻写入硬盘或者输出。在终端设备中,flush的作用也可能是丢弃缓存中得数据。

  • 无缓存(Unbuffered):不缓存输入或输出内容。例如,如果我们使用fputs函数输出15个字符,那么我们希望这15个字符尽可能快地被打印出来。如标准错误输出就要求是无缓存输出。

打开流

#include 
FILE *fopen(const char *restrict pathname, const char* restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int fd, const char *type);

系统数据信息和文件

进程环境

当内核执行C程序时(使用一个exec函数),调用main前先调用一个特殊的启动例程。可执行程序文件将启动例程指定为程序的起始地址——这由连接的编译器设置的

进程终止

5 种为正常终止方式:

1)从 main() 函数返回;

2)调用 exit(3) 函数;

3)调用 _exit(2) 或 _Exit(2) 函数;

4)最后一个线程从其启动例程返回;

5)从最后一个线程调用 pthread_exit(3) 函数。

剩下的 3 种为异常终止方式:

6)调用 abort(3) 函数;

7)接收到一个信号;

8)最后一个线程对取消请求作出响应。

环境表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ppGOUafZ-1596696659139)(/home/xiaohu/.config/Typora/typora-user-images/image-20200717222238017.png)]

C程序的存储空间布局

环境高级编程_第2张图片

分为几部分:

  • 正文段:CPU执行的机器指令部分,正文段是可共享的
  • 初始化数据段:通常称为数据段,包含程序中需明确地赋初值的变量
  • 未初始化数据段:bss段,内核将此段的数据初始化为0或空指针 long sum[100];
  • 栈:自动变量以及每次函数调用所需保存的信息都存放在此段中,
  • 在堆中进行动态储存分配

环境高级编程_第3张图片

共享库

使得可执行文件中不再需要包含公用的库函数,只需在所有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次执行或第一次调用某个库函数时,用动态链接的方法将程序与共享库函数相链接,减少了可执行文件的长度。但增加了运行时间的开销。

这种开销发生在程序第一次被执行时,或者每个共享库函数第一次被调用时,

共享库的优点可以使用库函数的新版本代替旧版本无需使用该库的程序重新连接编辑。

gcc -static hello.c:gcc的static参数阻止gcc编译使用共享库,这样编译出的程序比较大

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mc6R8fX1-1596696659144)(/home/xiaohu/.config/Typora/typora-user-images/image-20200717223700400.png)]

存储空间的分配

存储空间分配:

  • malloc 分配指定字节数的存储区。

  • calloc,为指定数量指定长度的对象分配存储空间

  • realloc:增加或减少以前分配区的长度。

环境高级编程_第4张图片

环境变量

#include 

char *getenv (const char *name); 此函数返回一个指针,它指向name=value,中的value。

函数setjmp和longjmp

在c中,goto语句是不能跨越函数的,而执行这类跳转功能的是函数setjmp和longjmp。对于处理发生在很深层嵌套函数调用中的出错情况是很有用的。

函数getrlimit和setrlimit

每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimit函数查询和更改

#include 
#include 
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

在更改资源限制时,须遵循下列三条规则:
1)任何一个进程都可以将一个软限制值更改为小于或等于其硬限制值
2)任何一个进程都可降低其硬限制值,但是它必须大于等于其软限制值。这种降低对于普通用户而言是不可逆的。
3)只有超级用户进程才能提高硬限制值

进程控制

一、函数fork

#include

pid_t fork(void) 子进程返回0,父进程返回子进程ID,出错返回-1

fork函数被调用一次,返回两次。先返回父进程还是子进程是不确定的,取决于内核使用的调度算法。

子进程和父进程并不共享存储空间,而是共享正文段。因此,子进程对变量所做的改变并不影响父进程中该变量的值。

父进程和子进程共享同一个文件偏移量。fork之后处理fd的两种情况:

(1)父进程等待子进程完成,当子进程完成操作后,它们任一共享的fd的文件偏移量,已经更新,父进程可以接着子进程继续工作。

(2)父进程和子进程各自执行不同的程序段,fork之后,父进程和子进程各自关闭它们不需要使用的fd,这样就不会干扰对方使用的fd,否则产生的文件偏移量会共享给对方,由于是执行不同的程序段,所以会产生干扰。(常用于网络服务进程,Socket通信中的server)

二、函数vfork

与fork的区别:

(1).vfork也是创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为vfork的目的是让子进程立即exec一个启动例程,这样,它也就不会引用该地址空间。

(2).vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,否则,会导致死锁。而fork之后父进程和子进程谁先执行是不确定的。

三、函数exit

进程的8种终止状态:

正常终止为:

(1).从main返回

(2).调用exit

(3)调用_exit或_Exit

(4)最后一个线程从其启动例程返回

(5)从最后一个线程调用pthread_exit

异常终止为:

(6)调用abort

(7)接到一个信号终止

(8)最后一个线程对取消请求做出响应

在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。

函数wait和waitpid

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号,因为子进程的终止状态是一个异步事件(可以在父进程运行的任何时候发生),这种信号也是内核像父进程发出的

waitpid有一选项可使调用者不阻塞

函数waitid

允许一个进程指定要等待的子进程

函数wait3和wait4

函数exec

fork创建新的子进程后,子进程往往调用一种exec函数以执行另一种程序。当进程调用exec函数时,该进程执行的程序完全替换为新程序,而新程序从main函数执行

exec只是用磁盘上的一个新程序替换当前进程的正文段、数据段、堆段和栈段

进程会计

大多数UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每个进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、所使用的CPU时间总量、用户和组ID、启动时间等。
会计记录结构定义在头文件

typedef u_short comp_t;
struct acct
{
    char ac_flag;
    char ac_stat;
    uid_t ac_uid;
    gid_t ac_gid;
    dev_t ac_tty;
    time_t ac_btime;
    comp_t ac_utime;
    comp_t ac_stime;
    comp_t ac_etime;
    comp_t ac_mem;
    comp_t ac_io;

    comp_t ac_rw;
    char ac_comm[8];

};

会计记录所需的各个数据(各CPU时间、传输的字节数等)都由内核保存在进程表中,并在一个新进程被创建时初始化。进程终止时写一个会计记录。这产生进程终止时写一个会计记录。这产生两个后果。

  • 我们不能获取永远不终止的进程的会计记录。像init这样的进程在系统生命周期中一直在运行,并不产生会计记录。这也同样适合于内核守护进程,他们通常不会终止。

  • 在会计文件中记录的顺序对应于进程终止的顺序,而不是他们启动的顺序。为了确定启动顺序,需要读全部会计文件,并按照日历时间进行排序。

会计记录对应于进程而不是程序。在fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时初始化。虽然exec并不创建一个新的会计记录,但相应记录中的命令名改变了,AFORK标志则被清除。这意味着,如果一个进程顺序执行了3个程序(A exec B、B exec C,最后是C exit),只会写一个进程会计记录。在该记录中的命令名对应于程序C,但是CPU时间是程序A、B、C之和。

用户标识

任一进程都可以得到其实际用户ID和有效用户ID及组ID。

但是,我们有时候希望找到运行该程序用户的登录名。我们可以调用getpwuid。

但是如果一个用户有多个登录名,这些登录名又对应着同一个用户ID,又将如何呢?可以用getlogin函数可以获取登陆此登录名

#include 
char *getlogin(void);
//返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL   

进程调度

调度策略和调度优先级是由内核确定的,进程可以通过nice值选择以更低的优先级运行(通过nice值降低它对CPU的占有)

nice函数

nice值越小,优先级低。

进程可以调用nice函数获取和更改它的nice值,使用这个函数,进程只影响自己的nice值,不能影响任何其他进程的nice值。

#include 
int nice(int incr);
//返回值:若成功,返回信的nice值NZERO;若出错,返回-1

incr参数被增加到调用进程的nice值。

  • 如果incr太大,系统直接把他降到最大合法值,不给出提示。
  • 如果incr太小,系统也会无声息的把他提高到最小合法值。

如果nice调用成功,并且返回值为-1,那么errno任然为0.如果errno不为0,说明nice调用失败。

getpriority函数

可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关进程的nice值

#include 
int getpriority(int which ,id_t who);   
//返回值:若成功,返回-NZERO~NZERO之间的nice值,若出错返回-1
  • which参数可以取下面三个值之一:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID。

  • who参数选择感兴趣的一个或者多个进程。如果who参数为0,表示调用进程、进程组或者用户(取决于which参数的值)。

当which设为PRIO_USER并who为0时,使用调用进程的实际用户ID。如果which参数作用于多个进程,则返回所有进程中优先级最高的。

setpriority函数

可以用于为进程、进程组和属于特定用户ID的所有进程设置优先级。

#include 
int setpriority(int which, id_t who, int value);   
//返回值:若成功,返回0;若出错,返回-1

参数which和who与getpriority相同。value增加到NZERO上,然后变为新的nice值。

进程时间

任一进程都可以调用times函数获取它自己以及终止子进程的墙上时钟时间、用户CPU时间和系统CPU时间。

#include 
clock_t times(struct tms *buf);
//返回值:若成功,返回流逝的墙上时钟时间;若出错,返回-1

此函数填写由buf指向的tms结构,该结构定义如下:

struct tms {   
    clock_t tms_utime;   
    clock_t tms_stime;
    clock_t tms_cutime;
    clock_t tms_cstime;
};

此结构没有包含墙上的时钟时间。times函数返回墙上时钟时间作为其函数值。此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值

例如,调用times,保存其返回值。在以后的某个时间再次调用times,从新返回的值减去以前返回的值,此差值就是墙上时钟时间。

所有由此函数返回的clock_t值都用_SC_CLK_TCK(由sysconf函数返回的每秒时钟滴答数)转换成秒数。

进程关系

终端登入

网络登录

进程组

每个进程除了有一进程ID外,还属于一个进程组

进程组是一个或多个进程的集合,同一进程组中的各进程接受来自同一终端的各种信号,每个进程有唯一的进程组ID

//返回调用进程的进程组ID  
#include   
pid_t getpgrp(void);  
pid_t getpgid(pid_t pid);  
//getpgid(0) 等于  getpgrp()  

每个进程组有一个组长进程。组长进程的进程组ID等于其进程ID。

进程组组长可以创建一个进程组、创建该组中的进程,然后终止。只要某个进程组中有一个进程存在,则该进程组就存在。

进程调用setpgid可以加入一个现有的进程组或创建一个新进程组

#include
int setpgid(pid_t pid,pid_t pgid);
//返回值:若成功,返回0;若出错,返回-1

setpgid将pid进程的进程组ID设置为pgid。

一个进程只能为它自己的子进程设置进程组ID,在它的子进程调用exec后,它就不再更改该子进程的进程组ID。

在大多数作业控制shell中,在fork后调用此函数,使父进程设置子进程的进程组ID,并且也使子进程设置其自己的进程组ID。

会话

会话是一个或多个进进程组的集合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9RrQNnku-1596696659147)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718153929779.png)]

通常shell的管道将几个进程编成一组

//创建一个新会话  
#include   
pid_t setsid(void);  
pid_t getsid(get_t pid);   //返回会话首进程的进程组ID  

对于setsid()函数,如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话

  • 该进程变成新会话的会话首进程(session leader会话首进程是创建该会话的进程),此时,该进程 是新会话中唯一进程
  • 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID
  • 该进程没有控制终端,如果在调用setsid之前该进程有一个控制终端,那么这种联系也被切断
    根据FD获取哪个进程组是前台进程组

控制终端

一个会话可以有一个控制终端

建立与控制终端连接的会话被称为控制进程

函数tcgetpgrp、tcsetgrp、tcgetsid

需要有一种方法通知内核哪一个进程是前台进程组,这样,终端设备驱动程序就能知道将终端输入和终端产生的信号发送到何处。

作业控制

允许在一个终端上启动多个作业(进程组),它控制一个作业可以访问该终端以及哪些作业在后台运行。要求以下三种形式的支持:

  • 支持作业控制的shell

  • 内核中的终端驱动程序必须支持作业控制

  • 内核必须提供对某些作业控制信号的支持

shell执行程序

孤儿进程组

孤儿进程:一个父进程已终止,这种进程由init进程“收养”

POSIX.1将孤儿进程组定义为:该组中的每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员

一个进程组不是孤儿进程的条件是:该组中有一个进程,其父进程在属于同一会话的另一组中

信号

信号是软件中断,信号提供了一种异步事件的方法

信号概念

每个信号都有一个名字,以SIG开头,在头文件中,信号被定义为正整数

编号为0的信号称为空信号

在某个信号

出现时,可以告诉内核按下列3中方式处理:

  • 忽略此信号:SIGKILL和SIGSTOP不能忽略,它们像内核和超级用户提供了使进程终止或停止的可靠的方法
  • 捕捉信号:waitpid
  • 执行系统默认动作:对于大多数信号的系统默认动作是终止该进程

一些信号详细说明:

  • SIGABRT 调用abort函数产生此信号,进程异常终止。

  • SIGCHLD 在一个信号终止或者停止时,这个信号发送给父进程。

  • SIGCONT 此信号发送给当前需要继续运行,而且处于停止状态的进程。

  • SIGEMT 指示一个现实定义的硬件故障。

  • SIGHUP 如果 终端接口检测到一个连接断开,发送到与终端相关的控制进程。

  • SIGKILL 这是两个不能被捕捉或者忽略的信号之一,向系统管理员提供杀死一个进程的可靠方法。

sinal函数

UNIX系统信号机制最简单的接口是signal函数。

#include 
void  (*signal(int  signo,  void  (*func) (int))) (int);
//若成功返回信号以前的处理配置,出错则返回SIG_ERR

1.程序启动,所有信号的状态都是系统默认或忽略。

exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变

2.进程创建

当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映射,所以信号捕捉函数的地址在子进程中是有意义的

不可靠的信号

不可靠是信号可能会丢失

中断的系统调用

当捕捉到某个信号时,被中断的是内核中执行的系统调用

将系统调用分2类:低速系统调用和其他系统调用

可冲入函数

进程捕捉到信号并对其进行处理,进程正在执行的程序被中断,首先执行该信号处理程序中的指令,如果从信号处理程序返回,则继续执行原有的被中断的程序

不可重入函数:

  • 他们使用静态数据结构
  • 他们调用malloc或free
  • 他们是标准I/O函数。

SIGCLD语义

子进程状态改变以后产生此信号,父进程需要调用一个wait函数以确定发生什么。

可靠信号术语和语义

当对信号采取某种动作时,我们说进程递送了一个信号,在产生信号和递送之间的时间间隔内,称信号是未决的。

进程调用sigpending函数来决定哪些信号是设置为阻塞并处于未决状态的。

进程可以调用sigprocmask来检测盒改变其当前信号屏蔽字。

kill和raise函数

kill函数将信号发送给进程或者进程组。raise函数允许进程向自己发送信号。

#include 
int kill (pid_t pid, int signo)
int raise(int signo)

alarm和pause函数

使用alarm函数可以设置一个计时器。当超过此计时器时,产生SIGALRM信号,如果吧忽略或不捕捉此信号,默认动作时终止调用此函数的进程。

#include  
unsigned  int   alarm (unsigned int seconds)
//seconds:产生SIGALRM需要经过的时钟秒数
//返回0或者以前设置的闹钟时间的剩余秒数。

当这一时刻到达时。信号由内核产生,由于进程调度的延迟,所以得到控制从而能够处理该信号还需要一个时间间隔

pause函数是调用进程挂起直至捕捉到一个信号。

#include 
int pause(void);

只要执行一个信号处理程序并从其返回,pause才返回,在这种情况下,pause返回-1,errno设置为EINTR

信号集

信号集类型是能表示多个信号的类型。可以用sigset_t以包含一个信号集。

#include 
#define _SIGSET_NWORDS(1024 / (8 * sizeof(unsigned long int)))
typedef struct
{
     unsignedlong int __val[_SIGSET_NWORDS];
}__sigset_t;

由定义可见,sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符集fd_set类似。Linux提供了如下一组函数来设置、修改、删除和查询信号集:

#include 
int sigemptyset(sigset_t *set);                  //清空信号集
int sigfillset(sigset_t *set);                   //在信号集中设置所有信号
int sigaddset(sigset_t *set, int signum);        //将信号signum添加到set信号集中
int sigdelset(sigset_t *set, int signum);        //删除信号
int sigismember(const sigset_t *set, intsignum); //测试信号是否在信号集中

函数sigprocmask

一个进程的屏蔽字规定了当前阻塞而不能传送给该进程的信号集。调用sigprocmask可以检测或更改、或同时检测和更改进程的信号屏蔽字

#include
intsigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//sigprocmask成功返回0,失败返回-1并设置errno
//_set参数指定新的信号掩码
//_oset参数则输出原来的信号掩码(如果不为NULL的话)

设置进程掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。

如下函数可以获得当前被挂起的信号集:

#include
intsigpending(sigset_t *set);
//set参数用于保存被挂起的信号集
//成功返回0,失败返回-1,设置errno

进程即使多次接收到同一个被挂起的信号,sigpending函数也只能反映一次。并且,当我们使用sigprocmask使被挂起的信号不被屏蔽时,该信号的处理函数也只能被触发一次。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9NN0OK1y-1596696659149)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718180456228.png)]

sigaction函数

设置信号处理函数更健壮的接口

#include 
int sigaction(int signum, const structsigaction *act, struct sigaction *oldact);
//signum参数之处要捕捉的信号类型
//act参数指定新的信号处理方式
//oldact参数则输出信号先前的处理方式(如果不为NULL的话)

act和oact都是sigaction结构体类型的指针,sigaction结构体描述了信号处理的细节,其定义如下:

struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
          };
//sa_handler成员指定信号处理函数
//sa_mask成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程,sa_mask是信号机sigset_t类型,该类型指定一组信号
//sa_flag成员用于设置程序收到信号时的行为
//sa_restorer成员已经过时,不在使用

环境高级编程_第5张图片

sigsetjmp和siglongjmp函数

#include  
int   sigsetjmp(sigjmp_buf   env,  int   savemask)
void   siglongjmp(sigjmp_buf   env,  int  val)

线程

每个线程都含有表示执行环境所必须的信息,其中包括进程中标识线程的线程ID, 一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。一个进程的所有信息对该进程的所有线程都是共享的,包括可执行代码、程序的全局内核和堆内存、栈以及文件描述符

线程标识

进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。

线程ID的类型是: pthread_t,是一个结构体数据类型,所以可移植操作系统实现不能把它作为整数处理。因此必须使用一个函数对两个线程ID进行比较:

#include 
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 若相等,返回非0数值;否则,返回0

使用 pthread_t 数据类型的 后果是不能用 一种可移植的方式打印该数据类型的值。

在程序调试中打印线程ID是非常有用的,而在其他情况下通常不需要打印线程ID。最坏的情况是有可能出现不可移植的调试代码,当然这也算不上是很大的局限性。

线程可以调用pthread_self函数获取自身的ID

#include 
pthread_t pthread_self(void); // 返回调用线程的线程ID

线程创建

#include 
int pthread_create(pthread_t *restrict tidp,
                              const pthread_attr_t *restrict attr,
                              void *(*start_rtn)(void *),
                              void *restrict arg);
// 返回值:成功返回0;否则返回错误编号
//注意:pthread函数在调用失败时通常会返回错误代码,他们并不像其他的 POSIX 函数一样设置 errno。

线程创建时并不能保证哪个线程会先运行:是新创建的线程,还是调用线程。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清楚

线程终止

  • 如果进程中的任意线程调用了 exit, _Exit, _exit 那么整个进程就会终止。

单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。

  • 线程可以简单地从启动例程中返回,返回值是线程的退出码。
  • 线程可以被同一进程中的其他线程取消。
  • 线程调用 pthread_exit .
#include 

void pthread_exit(void *rval_ptr);
// rval_ptr : 进程中的其他线程可以通过调用 pthread_join 函数访问这个指针。
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回值:成功返回0;否则返回错误编码

可用pthread_join函数控制线程的执行流

#include 
int pthread_join(pthread_t thread, void ** status);//成功时返回0,失败时返回其他值

//thread: thread所对应的线程终止后才会从pthread_join函数返回,换言之调用该函数后当前线程会一直阻塞到thread对应的线程执行完毕后才返回
//status:保存线程的main函数返回值的指针变量地址值

调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
如果线程只是从它的启动例程返回,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程:

#include 
int pthread_cancel(pthread_t tif); // 返回值:若成功则返回0,否则返回错误编号

pthread_detach

#include 
int pthread_detach(pthread_t thread);
//成功时返回0,失败时返回其他值
//thread:终止的同时需要销毁的线程ID

pthread_detach()即主线程与子线程分离,子线程结束后,资源自动回收

pthread_detach函数不会阻塞父线程,用于只是应用程序在线程thread终止时回收其存储空间。如果thread尚未终止,pthread_detach()不会终止该线程

线程同步

互斥量

互斥锁,也成互斥量,可以保护关键代码段,以确保独占式访问.当进入关键代码段,获得互斥锁将其加锁;离开关键代码段,唤醒等待该互斥锁的线程.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zAeZkTrN-1596696659151)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718191552955.png)]

#include 

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                        const pthread_mutexattr_t *restrict attr); // 默认 attr = NULL
int pthread_mutex_destory(pthread_mutex_t *mutex);

// 返回值:成功返回0,否则返回错误编号
 /* 
 * 函数功能:对互斥量进行加、解锁; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
int pthread_mutex_lock(pthread_mutex_t *mutex);//对互斥量进行加锁,线程被阻塞;  
int pthread_mutex_trylock(pthread_mutex_t *mutex);//对互斥变量加锁,但线程不阻塞;  
int pthread_mutex_unlock(pthread_mutex_t *mutex);//对互斥量进行解锁;  
/* 说明: 
 * 调用pthread_mutex_lock对互斥变量进行加锁,若互斥变量已经上锁,则调用线程会被阻塞直到互斥量解锁; 
 * 调用pthread_mutex_unlock对互斥量进行解锁; 
 * 调用pthread_mutex_trylock对互斥量进行加锁,不会出现阻塞,否则加锁失败,返回EBUSY。 

避免死锁

​ 若线程试图对同一个互斥量加锁两次,那么自身会陷入死锁状态,使用互斥量时,还有其他更不明显的方式也能产生死锁,例如,程序中使用多个互斥量,如果允许一个线程一直占有第一个互斥量,并且试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,这时就会发生死锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。

避免死锁的方法:

  • 控制互斥量加锁的顺序。
  • 使用“试加锁与回退”:在使用 pthread_mutex_lock() 锁住第一把锁的时候,其余的锁使用pthread_mutex_trylock() 来锁定,如果返回EBUSY,则释放前面占有的所有的锁,过段时间之后再重新尝试。

函数pthread_mutex_timedlock

函数pthread_mutex_timedlock互斥量原语允许绑定线程阻塞时间,当达到超时时间时,不对互斥量进行加锁,而是返回错误码ETIMEDOUT。

#include
#include
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);
//超时指定愿意等待的绝对时间(与相对时间对比而言,指定在时间x之前可以阻塞等待,而不是说愿意阻塞Y秒)。这个超时时间是用timespec结构来表示的,它用秒和纳秒来描述时间。

读写锁

读写锁(reader-writer lock)与互斥量类似,不过读写锁允许更高的并行性.

互斥量只有两种状态:加锁和不加锁,并且同一时刻只有一个线程对其加锁。读写锁有三种状态:读模式下加锁、写模式下加锁、不加锁;一次只有一个线程占用写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

  • 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞.

  • 当读写锁在读加锁状态时,所有试图以度模式对它进行加锁的线程都可以得以访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有线程释放他们的读锁为止.

读写锁非常适用于数据结构读的次数远大于写的情况

  • 当读写锁在写安全模式下时,它锁保护的数据结构就可以被安全的修改,因为一次只有一个线程可以在写模式拥有这个锁
  • 当读写锁在读模式下时,只要线程先获取读模式下的读写锁,该锁锁保护的数据结构可以被多个读模式下的锁共享

读写锁是一种共享独占锁:

  • 当读写锁以读模式加锁时,它是以共享模式锁住
  • 当以写模式加锁时,它以独占模式锁住

读写锁初始化和加解锁如下:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict *restrict attr);
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);
// 返回值:成功返回0,否则返回错误编号

静态读写锁:PTHREAD_RWLOCK_INITIALIZER

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读模式下锁定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写模式下锁定读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock
// 返回值:成功返回0,否则返回错误编号

各种实现可能会对共享模式下可获取的读写锁的次数进行限制,所以需要检查 pthread_rwlock_rdlock 的返回值.

定义了读写锁原语的条件版本

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 返回值:成功返回0,否则返回错误编号

可以获取锁时,这两个函数返回0,否则,返回错误EBUSY,这两个函数可以遵守锁层次但不能完全避免死锁的情况

带超时的读写锁

使应用程序在获取读写锁时避免陷入永久阻塞状态

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
                                const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
                                const struct timespec *restrict tsptr);
//tsptr参数指向timespec结构,指定线程应该的阻塞时间,与pthread_mutex_timedlock类似
// 返回值:成功返回0;否则返回错误编号

条件变量

  • 条件本身是由互斥量保护的.线程在该表条件状态之前必须首先锁住互斥量.
  • 使用条件变量之前,必须先对它初始化.
int pthread_cond_init(pthread_cond_t *restrict cond,
                    const pthread_condattr_t *restrict attr);
int pthread_cond_destory(pthread_cond_t *cond);
// 返回值:成功返回0;否则返回错误编号

可以把常量PTHREAD_COND_INITIALIZER(只是把条件变量的各个字段都初始化0)赋给静态分配的条件变量

如果条件变量是动态分配的,需要使用pthread_cond_init函数对它初始化

int pthread_cond_init(pthread_cond_t *restrict cond,
                    const pthread_condattr_t *restrict attr);
int pthread_cond_destory(pthread_cond_t *cond);
// 返回值:成功返回0;否则返回错误编号
int pthread_cond_wait(pthread_cond_t *restrict cond,
                        pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                            pthread_mutex_t *restrict mutex,
                            const struct timespec *restrict tsptr); // 超时返回ETIMEDOUT
// 返回值:成功返回0;否则返回错误编号

在调用pthread_cond_wait前。必须确保互斥量mutex加锁,以确保pthread_cond_wait操作的原子性。pthread_cond_wait函数执行时,首先调用线程放入条件变量的等待队列中,将互斥量mutex解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何改变。

int pthread_cond_signal(pthread_cond_t *cond);   // 唤醒一个等待该条件的线程
int ptrhead_cond_broadcast(pthread_cond_t *cond); // 唤醒等待该条件的所有线程
// 返回值:成功返回0;否则返回错误编号

自旋锁

自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)

环境高级编程_第6张图片

自旋锁和互斥量类似,但是它不能通过睡眠使进程阻塞,而是在获取锁之前一尺处于忙阻塞状态.

适用情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本.

使用自旋锁会有以下一个问题:

  • 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

  • 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destory(pthread_spinlock_t *lock);
// 返回值:成功返回0;否则返回错误编号

// pshared: 表示进程共享属性,表明自旋锁是如何获取的. PTHREAD_PROCESS_SHARED 则自旋锁能被可以访问锁底层内存的线程所获取,几遍那些线程属于不同的进程.
// PTHREAD_PROCESS_PRIVATE: 自旋锁只能被初始化该锁的进程内部的线程访问.
int pthread_spin_lock(pthread_spinlock_t *lock);
int ptrhead_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
// 返回值:成功返回0;否则返回错误编号

屏障

屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。
  屏障允许任意数量的线程等待,知道所有的线程完成处理工作,而线程不需要退出。所有线程到达屏障之后可以接着工作。
  可以使用pthread_barrier_init函数对屏障进行初始化,用pthread_barrier_destroy函数进行反初始化。

#include 
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
            const pthread_barrierattr_t *restrict attr,
            unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

初始化屏障时,参数count 指定:在允许所有的线程继续运行前,必须到达屏障的线程数目.

int pthread_barrier_wait(pthread_barrier_t *barrier);
// 返回值:成功返回0;否则返回错误编号
  • 对于任一线程,pthread_barrier_wait 函数返回了 PTHREAD_BARRIER_SERIAL_THREAD. 剩余的线程看到的返回值为0.这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上.
  • 一旦达到屏障的计数值,而且线程处于非阻塞状态,屏障可以被重用.

线程控制

线程限制

Single Unix定义了一线线程操作的限制,和其他的限制一样,可以通过sysconf来查询。和其它的限制使用目的一样,为了应用程序的在不同操作 系统的可移植性。 一些限制:

PTHREAD_DESTRUCTOR_ITERATIONS: 销毁一个线程数据最大的尝试次数,可以通过_SC_THREAD_DESTRUCTOR_ITERATIONS作为sysconf的参数查询。
PTHREAD_KEYS_MAX: 一个进程可以创建的最大key的数量。可以通过_SC_THREAD_KEYS_MAX参数查询。
PTHREAD_STACK_MIN: 线程可以使用的最小的栈空间大小。可以通过_SC_THREAD_STACK_MIN参数查询。
PTHREAD_THREADS_MAX:一个进程可以创建的最大的线程数。可以通过_SC_THREAD_THREADS_MAX参数查询 

同步属性

互斥量属性

有进程共享属性和类型属性两种,进程共享属性是可选的,互斥量属性数据类型为pthread_mutexattr_t。在进程中,多个线程可以访问同一个同步对象,默认情况进程共享互斥量属性为:PTHREAD_PROCESS_PRIVATE

读写锁属性

与互斥量类似,但是只支持进程共享唯一属性,操作函数原型如下:

条件变量属性

只支持进程共享属性

屏障属性

重入

有了信号处理程序和多线程,多个控制线程在同一时间可能潜在的调用同一个函数,如果一个函数在同一时刻可以被多个线程安全调用,则称为函数是线程安全的。很多函数并不是线程安全的,因为它们返回的数据是存放在静态的内存缓冲区,可以通过修改接口,要求调用者自己提供缓冲区使函数变为线程安全的。POSIX.1提供了以安全的方式管理FILE对象的方法,使用flockfile和ftrylockfile获取与给定FILE对象关联的锁。这个锁是递归锁。函数原型如下:

void flockfile(FILE *filehandle);
int ftrylockfile(FILE *filehandle);
void funlockfile(FILE *filehandle);

为了避免标准I/O在一次一个字符操作时候频繁的获取锁开销,出现了不加锁版本的基于字符的标准I/O例程。函数如下:

int getc_unlocked(FILE *stream);
int getchar_unlocked(void);
int putc_unlocked(int c, FILE *stream);
int putchar_unlocked(int c);

线程特定数据

线程特定数据:是存储和查询与某个线程相关的数据的一种机制,希望每个线程可以独立的访问数据副本,而不需要担心与其他线程的同步访问问题。进程中的所有线程都可以访问进程的整个地址空间,除了使用寄存器以外,线程没有办法阻止其他线程访问它的数据,线程似有数据也不例外。管理线程私有数据的函数可以提高线程间的数据独立性。

分配线程私有数据过程:首先调用pthread_key_create创建与该数据关联的键,用于获取对线程私有数据的访问权,这个键可以被进程中所有线程访问,但是每个线程把这个键与不同的线程私有数据地址进行关联然后通过调用pthread_setspecific函数吧键和线程私有数据关联起来,可以通过pthread_getspecific函数获取线程私有数据的地址。

取消选项

取消选项包括可取消状态和可取消类型,针对线程在响应pthread_cancel函数调用时候所呈现的行为。可取消状态取值为:PTHREAD_CANCLE_ENABLE (默认的可取消状态)或PTHREAD_CANCLE_DISABLE。取消类型也称为延迟取消,类型可以为:PTHREAD_CANCLE_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。通过下面函数进行设置取消状态和取消类型:

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void); //自己添加取消点

线程和信号

线程和fork

线程和I/O

守护进程

守护进程也称为精灵进程是一种生存期较长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们没有控制终端,常常在系统引导装入时启动,在系统关闭时终止。unix系统有很多守护进程,大多数服务器都是用守护进程实现的,例如inetd守护进程。

守护进程的特征

用ps命令察看一些常用的系统守护进程,看一下他们和几个概念:进程组、控制终端和会话有什么联系,执行: ps –efj

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CCl3Jupd-1596696659156)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718231133224.png)]

从结果可以看出守护进程没有控制终端,其终端名设置为?,终端前台进程组ID设置为-1,init进程ID为1。系统进程依赖于操作系统实现,父进程ID为0的各进程通常是内核进程,它们作为系统自举的一部分而启动。内核进程以超级用户特权运行,无控制终端,无命令行。大多数守护进程的父进程是init进程。

守护进程与后台进程的区别:(1) 后台运行程序,即加&启动的程序,(2)后台运行的程序拥有控制终端,守护进程没有

高级I/O

环境高级编程_第7张图片

非阻塞I/O

非阻塞 I/O 使我们可以调用 open、write 和 read 这样的 I/O 操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则立即出错返回,表示该操作若继续执行将阻塞。
对于一个给定的文件描述符由以下两种方法可以对其指定非阻塞 I/O:

  • 若调用 open 获得描述符,则可指定 O_NONBLOCK 标志;

  • 对已打开的描述符,可以使用 fcntl,由该函数打开 O_NONBLOCK 文件状态标志;

记录锁

当多个进程在编辑同一个文件时,在 UNIX 系统中,文件的最后状态取决于写该文件的最后一个进程,但是进程必须要确保它正在单独写一个文件,所以需要用到记录锁机制。

记录锁的功能:当一个进程在读或修改文件的某一部分时,它可以阻止其他进程修改同一个文件区,记录锁也称为字节范围锁,因为它锁定的只是文件中的一个区域或整个文件。

fcntl 记录锁

记录锁可以通过 fcntl函数进行控制,该函数的基本形式如下:

/* fcntl记录锁 */  
/* 
 * 函数功能:记录锁; 
 * 返回值:若成功则依赖于cmd的值,若出错则返回-1; 
 * 函数原型: 
 */  
#include   
int fcntl(int fd, int cmd, .../* struct flock *flockptr */);  
/* 
 * 说明: 
 * cmd的取值可以如下: 
 * F_GETLK              获取文件锁 
 * F_SETLK、F_SETLKW    设置文件锁 
 * 第三个参数flockptr是一个结构指针,如下: 
 */  
struct flock  
{  
    short l_type;       /* F_RDLCK, F_WRLCK, F_UNLCK */  
    off_t l_start;      /* offset in bytes, relative to l_whence */  
    short l_whence;     /* SEEK_SET, SEEK_CUR, SEEK_END */  
    off_t l_len;        /* length, in bytes; 0 means lock to EOF */  
    pid_t l_pid;        /* returned with F_GETLK */  
};  

I/O多路转接(I/O 复用)

select

poll

epoll:

epollLinux 上特有的 I/O 复用函数 。

epollpollselect差异

  • epoll 使用一组函数来完成任务,而不是单个函数
  • epoll 把文件描述符事件放在 内核事件表 当中,从而无需像 selectepoll 那样每次调用都要重复传入描述符集或事件集
  • epoll 需要一个额外文件描述符,来唯一标识 内核事件表
  • epoll 采用回调方法来检测就绪事件,而 selectepoll 采用轮询方式,复杂度更高

LTET 模式:

  • LT 模式 :即 电平触发 ,是默认的工作方式,当 epoll_wait 检测到有事件发生并通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用 epoll_wait 时,epoll_wait 还会通知此事件,直到此事件被解决 。( 此模式下,epoll 相当于一个效率更高的 poll
  • ET 模式:即 边缘触发 ,当往内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll 将以 ET 模式来操作该文件描述符,它是 epoll 的高效工作模式 。当 epoll_wait 检测到有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序通知这一事件 。可见,ET 模式 降低了同一个事件重复触发的次数,因此效率更高 。

EPOLLONESHOT 事件:对于注册了 EPOLLONESHOT 事件的文件描述符,系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次 。这样,当一个线程在处理某个文件描述符时,其他线程是不可能有机会操作该文件描述符的 。

三组 I/O 复用函数的比较

环境高级编程_第8张图片

POSIX 异步 I/O

异步 I/O 作用:在执行 I/O 操作时,如果还有其他事务要处理而不想被 I/O 操作阻塞,就可以使用异步 I/O 。

异步 I/O 接口使用 AIO 控制块 来描述 I/O 操作 。aiocb 结构定义了 AIO 控制块

在进行异步 I/O 之前需要先 初始化 AIO 控制块 ,调用 aio_read 函数来进行 异步读 操作,或调用 aio_write 函数来进行 异步写 操作:

#include 
int aio_read(struct aiocb *aiocb);	
int aio_write(struct aiocb *aiocb);
//返回值:若成功,返回 0;若出错,返回 -1

函数 readv 和 writev

readvwritev 函数用于在一次函数调用中 读、写多个非连续缓冲区 。也将这两个函数称为 散布读聚集写

#include 
ssize_t readv(int fd, const struct iovec *iov, int iovent);
ssize_t writev(int fd, const struct iovec *iov, int iovent);
//返回值:已读或已写的字节数;若出错,返回 -1

iovec 结构:

struct iovec {
    void *iov_base;	// starting address of buffer
    size_t iov_len;	// size of buffer
};

环境高级编程_第9张图片

writev 函数从缓冲区中聚集输出数据的顺序是:[0] 、 直至 writev 返回输出的字节总数,通常应等于所有缓冲区长度之和 。

readv 函数则将读入的数据按上述同样顺序散布到缓冲区中 。readv 总是先填满一个缓冲区,然后填下一个。readv 返回读到的字节总数,如果遇到文件尾端,已无数据可读,则返回 0

存储映射 I/O

存储映射 I/O 能将一个磁盘文件映射到存储空间的一个缓冲区上 ,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节;将数据写入缓冲区时,相应字节就自动写入文件。这样就可以在不使用 readwrite 的情况下执行 I/O 。

mmap 函数将一个给定的文件映射到一个存储区域中:

#include 
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);
//返回值:若成功,返回映射区的起始地址;若出错,返回 MAP_FAILED

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rmfvKtpF-1596696659163)(/home/xiaohu/.config/Typora/typora-user-images/image-20200719104158035.png)]

环境高级编程_第10张图片

进程间通信

https://www.cnblogs.com/brianleelxt/p/13225313.html

环境高级编程_第11张图片

进程间通信 (IPC) 是进程之间相互通信的技术:

前十种 IPC 形式通常限于同一台主机的两个进程之间的 IPC ,最后两行( 套接字和 STREAMS )是仅有的支持不同主机上两个进程之间的 IPC 的两种形式 。

管道

管道的两种 局限性

  • 有的系统仅提供半双工通道
  • 管道只能在具有公共祖先的两个进程之间使用 。通常,一个管道由一个进程创建,在进程调用 fork 之后,这个管道就能在父进程和子进程之间使用了

管道是通过调用 pipe 函数 创建 的:

#include 
int pipe(int fd[2]);
//返回值:若成功,返回 0;若出错,返回 -1

经由参数 返回两个文件描述符:[0] 为读而打开,[1] 为写而打开 。[1] 的输出是 [0]的输入 。

单个进程中的管道几乎没有任何用处 。通常,进程会先调用 pipe ,接着 fork ,从而创建从父进程到子进程的 IPC 通道 。对于 父进程到子进程的管道 ,父进程关闭管道的读端 [0],子进程关闭写端 [1] (如下图所示)。子进程到父进程的管道 反之 。

环境高级编程_第12张图片

函数 popen 和 pclose

两个函数实现的操作:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。

popen 函数的作用是:先执行 fork 创建子进程,然后调用 exec 执行

,并且返回一个标准 I/O 文件指针 :

#include 
FILE *popen(const char *cmdstring, const char *type);
//返回值:若成功,返回文件指针;若出错,返回 NULL

如果 r ,则文件指针连接到 的标准输出( 返回的文件指针是可读的 );若 w ,则文件指针连接到 的标准输入( 返回的文件指针是可写的 )

环境高级编程_第13张图片

pclose 函数关闭标准 I/O 流,等待命令终止,然后返回 shell 的终止状态:

#include 
int pclose(FILE *fp);
//返回值:若成功,返回 cmdstring 的终止状态;若出错,返回 -1

如果 shell 不能执行,则 pclose 返回值的终止状态与 shell 已经执行 exit(127) 一样

用法举例fp = popen("cmd 2>&1", "r");

协同进程

过滤程序 从标准输入读取数据,向标准输出写数据,几个过滤程序通常在 shell 管道中线性连接 。当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了 协同进程

popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道。协同进程有连接到另一个进程的两个单向通道:一个连接到其标准输入,另一个来自其标准输出

我们向将数据写到其标准输入,经其处理后,再从其标准输出读取数据。

环境高级编程_第14张图片

进程创建两个管道:一个是协同进程的标准输入,一个是协同进程的标准输出

FIFO

FIFO 有时被称为 命名管道 。未命名的管道只能在两个相关的进程之间使用,而且这两个相关进程还要有一个共同创建了它们的祖先进程 。但是,通过 FIFO不想关的进程也能交换数据

FIFO 是一种文件类型,创建 FIFO 类似于创建文件

#include 
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
//返回值:若成功,返回 0;若出错,返回 -1

FIFO 有以下两种用途:

  • shell 命令使用 FIFO 将数据从一条管道传送到另一条时,无需创建临时文件
  • 客户进程-服务器进程 应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程二者之间传递数据

XSI IPC

有 3 种称作 XSI IPCIPC :消息队列、信号量以及共享存储器 。

每个 内核中IPC 结构(消息队列、信号量或者共享存储段)都用一个非负整数的 标识符 加以引用 。如:要向一个消息队列发送消息或者从一个消息队列去消息,只需要知道其队列标识符 。

标识符 是 IPC对象 的内部名 。为使多个合作进程能够在同一 IPC对象上汇聚,需要提供一个外部命名方案 。为此,每个IPC对象都与一个 相关联,将这个键作为该对象的外部名 。无论何时创建 IPC结构(通过调用 msggetsemgetshmget 创建),都应指定一个键 ,这个键由内核编程标识符 。

使 客户进程和服务进程在同一 IPC 结构上汇聚 的方法:

  • 服务器进程可以指定键 IPC_PRIVATE 创建一个新 IPC 结构,将返回的标识符存放在某处( 如一个文件 )以便客户进程取用 。缺点是:文件系统操作需要服务器进程将整型标识符写到文件中,此后客户进程又要读这个文件取得此标识符
  • 客户进程和服务器进程认同一个 路径名项目ID (0~255之间的字符值) ,接着调用 ftok 将这两个值变换为一个 ,然后服务器进程特定此键创建一个新的 IPC 结构

ftok 函数作用为:由一个路径名和项目 ID 产生一个键:

#include 
key_t ftok(const char *path, int id);	
//path必须引用一个现有文件;当产生键时,只使用id参数的低 8 位
//返回值:若成功,返回键;若出错,返回 -1

消息队列

消息队列 是消息的链接表,存储在内核中,由 消息队列标识符 标识 。

msgget 用于创建一个新队列或打开一个现有队列:

#include 
int msgget(key_t key, int flag);
//返回值:若成功,返回消息队列 ID;若出错,返回 -1

msgsnd将新消息添加到队列尾端

每个消息由 3 部分组成:一个正的长整型类型的字段、一个非负的长度( )以及实际数据字节数(对应于长度)。这些都在添加到队列中队列时,传给msgsnd,msgrcv用于从消息队列中取消息。

#include 
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
//返回值:若成功,返回消息数据部分的长度;若出错,返回 -1

参数指向一个长整型数(储存的是返回的消息类型),其后跟随的是存储实际消息的缓冲区。nbytes是缓冲区的长度。

信号量

信号量 是一个计数器,用于为多个进程提供对共享数据对象的访问 。信号量通常在内核中实现 。

信号量 实际上是同步原语而不是 IPC ,常用于共享资源(如共享存储段)的同步访问

对于共享资源的获取和释放 ,进程需要执行下列操作:

  1. 测试控制该资源的信号量

  2. 若此信号量的值为正,则进程可以使用该资源 。这种情况下,进程会将信号量值减 1,表示它使用了一个资源单位

  3. 否则,若此信号量的值为 0,则进程进入休眠状态,直至信号量值大于 0 。进程被唤醒后,返回步骤 (1)

  4. 当进程不再使用由一个信号量控制的共享资源时,该信号量值增 1 。如果有进程正在休眠等待此信号量,则唤醒它们

内核为每个 信号量集合 维护着一个 semid_ds 结构:

struct semid_ds {
    struct ipc_perm sem_perm;	//规定权限和所有者
    unsigned short sem_nsems;	//集合中信号量的编号
    time_t sem_otime;			
    time_t sem_ctime;
    //...
};

每个信号量 由一个无名结构表示,它至少包含下列成员:

struct {
    unsigned short semval;	//信号量的值,>= 0
    pid_t sempid;			//上次操作的 pid
    unsigned short semncnt;	//semncnt 个进程在等待 semval > curval (curval为sembuf中的sem_op)
    unsigned short semzcnt;	//semzcnt 个进程在等待 semval == 0
};

要使用 XSI 信号量 时,首先需要通过调用函数 semget 来获得一个 信号量 ID

#include 
int semget(key_t key, int nsems, int flag);
//返回值:若成功,返回信号量ID;若出错,返回 -1

信号量、记录锁和互斥量的时间比较

在记录上,记录锁比信号量快,但在共享存储中互斥量的性能比信号量和记录锁都要优越。但在多个进程间共享的内存使用互斥量来恢复一个终止的进程更难。

共享存储

共享存储 允许两个或多个进程共享一个给定的存储区 。因为数据不需要在客户进程和服务器进程之间复制,所以这是 最快 的一种 IPC 。使用共享存储区,要在多个进程之间 同步 访问一个给定的存储区 。通fanhui常,信号量 用于同步共享存储访问 。

XSI 共享存储和内存映射的文件的不同之处在于,前者没有相关的文件 。XSI 共享存储段 是内存的匿名段

内核为每个 共享存储段 维护着一个结构:

struct shmid_ds {
    struct ipc_perm shm_perm;	// 规定权限和所有者
    size_t shm_segsz;			// size of segment in bytes
    pid_t shm_lpid;				// pid of last shmop
    pid_t shm_cpid;				// pid of creator
    shmatt_t shn_nattch;		// 共享存储段的连接计数
    time_t shm_atime;			// last-attach time
    time_t shm_dtime;			// last-detach time
    time_t shm_ctime;			// last-change time
    //...
};

shmget 函数用于 获得一个共享存储标识符

#include 
int shmget(key_t key, size_t size, int flag);
//返回值:若成功,返回共享存储 ID;若出错,返回 -1

进程连接共享存储段之后的存储区示意图

环境高级编程_第15张图片

POSIX 信号量

POSIX 信号量意在解决 XSI 信号量 的几个缺陷:

  • POSIX 信号量新能更高
  • POSIX 信号量接口使用更简单(没有信号量集),会使用文件系统进行实现,一些接口被模式化
  • POSIX 信号量在删除时表现更完美 。当一个 XSI 信号量被删除时,使用这个信号量标识符的操作会失败,并将 errno 设置成 EIDRM 。而使用 POSIX 信号量时,操作能继续正常工作直到该信号量的最后一次引用被释放

命名信号量未命名的信号量(差异在于创建和销毁形式):

  • 未命名信号量:只存在与内存中,并要求能使用信号量的进程必须可以访问内存。( 这意味着它只能应用在同一进程的线程中,或者不同进程中已经映射相同内存内容到它们的地址空间中的线程 )
  • 命名信号量:可以通过名字访问,因此可以被任何已知它们名字的进程中的线程使用

几种 IPC 的应用

  • 管道FIFO 仍可有效应用于大量的应用程序
  • 新的应用程序中,应尽可能 避免使用 消息队列 和 信号量 ,而应考虑使用 全双工管道记录锁 ,它们使用起来更方便
  • 共享存储仍有它的用途,虽然通过 mmap 函数也能提供同样的功能

网络IPC:套接字

1. 网络 IPC

经典进程间通信机制IPC ):管道、FIFO 、消息队列、信号量和共享存储,允许在同一台计算机上运行的进程之间通信 。

网络进程间通信 是不同计算机(通过网络连接)上的进程相互通信的机制 。(也可用于计算机内通信)

2. 套接字描述符

套接字 是通信端点的抽象,应用程序用 套接字描述符 访问套接字 。套接字描述符在 UNIX 系统中被当作是一种文件描述符 。

使用 socket 函数 创建 一个套接字:

#include 
int socket(int domain, int type, int protocol);
//返回值:若成功,返回套接字描述符;若出错,返回 -1

不再需要套接字时,使用 close 函数 关闭对套接字的访问 ,并且释放该描述符以便重新使用 。

套接字通信是双向的 ,可以采用 shutdown 函数来禁止一个套接字的 I/O

#include 
int shutdown(int sockfd, int how);
//返回值:若成功,返回 0;若出错,返回 -1

如果 SHUT_RD (关闭读端),那么无法从套接字读取数据;如果 SHUT_WR (关闭写端),那么无法使用套接字发送数据 ;如果 SHUT_RDWR ,则既无法读取数据,又无法发送数据 。

使用 shutdown 的原因

  • 只有最后一个文件描述符引用被关闭时,close 才释放网络端点;而 shutdown 允许使一个套接字处于不活动状态,和引用它的文件描述符数目无关
  • 可以很方便地关闭套接字双向传输中的一个方向

网络字节序

内存中存储两个字节有两种方法

  • 将低序字节存储在起始地址,成为小端
  • 将高序字节存储在起始地址,成为大端

代表CPU数据保存方式的主机字节序在不同的CPU中也各不相同,目前主流CPU一小端序方式保存数据

在网络传输数据约定统一方式,这种约定成为网络字节序,非常简单——统一为大端序。即先把数据数组转化为大端序列格式再进行传输,所有计算机接受数据时应识别该数据是网络字节序格式,小端序系统传输数据应转为大端序排列方式。

lisen

listen函数:把一个未连接的套接字转为一个被动套接字,指示内核应接受的连接请求,调用listen导致套接字从CLOSING状态转换到LISTEN状态

内核为任何一个给定的监听套接字维护两个队列

  • 未完成连接队列:客户像服务器发出请求达到服务器,SYN=1,初始化一个序号,服务器端套接字处于SYN_RCVD状态。
  • 已完成连接队列:已完成TCP三次握手,处于ESTANLISHED状态
#include 
/*  sockfd --socket()函数返回的服务器端套接字描述符  */
/*  backlog --连接请求等待队列的长度*/
int listen(int sockfd, int b
acklog);
//若成功返回0,反之返回-1
//backlog未两个队列总和的最大值

accept

TCP服务器调用,返回已完成连接队列头返回下一个已完成连接。如果已完成的连接队列为空,进程背投入睡眠

/*  sockfd --socket()函数返回的服务器端的套接字描述符  */
/*  cliaddr --保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息*/
/*  addrlen --调用前:为cliaddr结构体的长度,但是存有长度的变量地址*/
/*          --调用后:客户端地址长度  */
int accept(int sockfd, struct sockaddr *cliaddr , socklen_t *addrlen);//若成功返回非负描述符,若出错,则返回-1

参数cliaddr和addrlen返回已连接的对端进程(客户)的协议地址

数据传输

带外数据

带外数据 允许更高优先级的数据传输 。带外数据 先行传输 ,即使传输队列已经有数据 。TCP 支持带外数据,但是 UDP 不支持 。

TCP 将带外数据称为 紧急数据 ,仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输 。

你可能感兴趣的:(TCP/IP网咯编程)