C++学习记录009——Linux系统编程下篇

系列文章目录

第一章 C语言基础知识
第二章 C语言高级编程
第三章 C语言数据结构
第四章 C语言高级数据结构
第五章 C++核心编程
第六章 C++STL
第七章 Qt桌面应用开发
第八章 Linux系统编程上篇
第九章 Linux系统编程下篇



一、进程控制

  1. 进程相关概念:程序和进程、并行和并发、PCB-进程控制块、进程状态。

    • 程序,是指编译好的二进制文件,在磁盘上,占用磁盘空间, 是一个静态的概念。进程,一个启动的程序, 进程占用的是系统资源,如:物理内存,CPU,终端等,是一个动态的概念。同一个程序可以加载为不同的进程(彼此之间互不影响)。
    • 并发,在一个时间段内, 是在同一个cpu上, 同时运行多个程序。并行性指两个或两个以上的程序在同一时刻发生(需要有多颗cpu)。
    • 每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。其内部成员有很多,重点掌握以下部分:
      • 进程id,系统中每个进程有唯一的id,是一个非负整数。进程的状态,有就绪、运行、挂起、停止等状态。进程切换时需要保存和恢复的一些CPU寄存器。描述虚拟地址空间的信息。描述控制终端的信息。当前工作目录。umask掩码。文件描述符表,包含很多指向file结构体的指针。和信号相关的信息。用户id和组id。会话(Session)和进程组。进程可以使用的资源上限:ulimit -a
    • 进程状态:进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
      在这里插入图片描述
  2. 创建进程:fork函数、ps命令和kill命令、getpid/getppid。

    • fork函数创建子进程。pid_t fork(void);返回值:调用成功:父进程返回子进程的PID,是一个大于0的数,子进程返回0;调用失败:返回-1,设置errno值。注意的是:不是fork函数在一个进程中返回2个值,而是在父子进程各自返回一个值。
      在这里插入图片描述
    • 子进程创建成功后,代码的执行位置:父进程执行到什么位置,子进程就从哪里执行。
    • ps aux | grep "xxx";ps aux | grep "xxx",kill -l 查看系统有哪些信号,kill -9 pid 杀死某个线程。
    • -a:当前系统所有用户的进程;-u:查看进程所有者及其他一些信息;-x:显示没有控制终端的进程 – 不能与用户进行交互的进程【输入、输出】;-j: 列出与作业控制相关的信息。
    • getpid - 得到当前进程的PID,pid_t getpid(void); getppid - 得到当前进程的父进程的PID,pid_t getppid(void);
    • 循环创建多个子进程:若让多个子进程都是兄弟进程,必须不能让子进程再去创建新的子进程。
      在这里插入图片描述
    • 父子进程不能共享全局变量。如果父子进程只是对全局变量做读操作,父子进程在内存中只有一份,属于共享。但是如果父子进程中任何一个进程对该全局变量进行修改操作,其会在内存中拷贝一个副本,然后在副本上进行修改,修改完成后映射回去。
      在这里插入图片描述
  3. exec函数族:有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序,此时就用到了exec函数族当中的函数。使用方法一般都是在父进程里面调用fork创建子进程,然后在子进程里面调用exec函数。包括execl函数和execlp函数,execlp函数一般是执行系统自带的程序或者是命令。

    int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
    执行的程序的绝对路径、要执行的程序的需要的参数、占位,通常写应用程序的名字、arg后面的: 命令的参数、参数写完之后: NULL
    若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行execl后面的代码,可以用perror打印错误原因
    
    int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
    file执行命令的名字, 根据PATH环境变量来搜索该命令、占位、arg后面的: 命令的参数、参数写完之后: NULL
    若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行exec后面的代码,可以用perror打印错误原因
    
    
    • exec函数族原理:如:execlp(“ls”, “ls”, “-l”, NULL)。exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。
      在这里插入图片描述
    • 当execl和execlp函数执行成功后,不返回,并且不会执行execl后面的代码逻辑,原因是调用execl函数成功以后,exec函数指定的代码段已经将原有的代码段替换了。
  4. 进程回收:当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。

    • 孤儿进程:若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程。
    • 为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
    • 僵尸进程:若子进程死了,父进程还活着, 但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。
    • 由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死。通过杀死其父进程的方法可以消除僵尸进程,杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。
    • 进程回收函数wait和waitpid作用:阻塞并等待子进程退出,回收子进程残留资源,获取子进程结束状态(退出原因)。
      pid_t wait(int *status);
      status参数:子进程的退出状态 -- 传出参数
      WIFEXITED(status):为非0        → 进程正常结束
      WEXITSTATUS(status):获取进程退出状态 
      WIFSIGNALED(status):为非0 → 进程异常终止
      WTERMSIG(status):取得进程终止的信号编号
      成功:清理掉的子进程ID。失败:-1 (没有子进程)
      
      pid_t waitpid(pid_t pid, int *status, in options);
      pid = -1 等待任一子进程。与wait等效
      pid > 0 等待其进程ID与pid相等的子进程
      pid = 0 等待进程组ID与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程
      pid < -1 等待其组ID等于pid的绝对值的任一子进程。(适用于子进程在其他组的情况)
      status: 子进程的退出状态,用法同wait函数
      options:设置为WNOHANG,函数非阻塞,设置为0,函数阻塞
      >0:返回回收掉的子进程ID; -1:无子进程;=0:参3为WNOHANG,且子进程正在运行
      
      

二、进程间通信

  1. 进程间通信:Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
    在这里插入图片描述

  2. 进程间通信的方式:在进程间完成数据传递需要借助操作系统提供特殊的方法,如:管道、信号、共享内存、消息队列、套接字、命名管道等。现今常用的进程间通信方式有:管道 (使用最简单),信号 (开销最小),共享映射区 (无血缘关系),本地套接字 (最稳定)。

  3. 管道-pipe:管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。

    • 管道的实质:是内核缓冲区,内部使用环形队列实现。由两个文件描述符引用,一个表示读端,一个表示写端。规定数据从管道的写端流入管道,从读端流出。当两个进程都终结的时候,管道也自动消失。管道的读端和写端默认都是阻塞的。默认缓冲区大小为4K,可以使用ulimit -a命令获取大小,实际操作过程中缓冲区会根据数据压力做适当调整。
    • 管道的局限性:数据一旦被读走,便不在管道中存在,不可反复读取。数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道,只能在有血缘关系的进程间使用管道。
    • 创建管道函数:int pipe(int fd[2]) 若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端,向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。成功返回0,失败返回-1,并设置errno值。
    • 父子进程使用管道通信:一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信。可以推出只要两个进程中存在血缘关系(具有共同的祖先),都可以采用管道方式来进行通信。父子进程间具有相同的文件描述符,且指向同一个管道pipe,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。
    • 具体步骤:1 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]和fd[1],分别指向管道的读端和写端。2 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管。3 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出,这样就实现了父子进程间通信。
      在这里插入图片描述
    • 父子进程间通信, 实现ps aux | grep bash:创建管道和子进程,父进程关闭读端,子进程关闭写端,父进程中将标准输出重定向到管道写端。
      在这里插入图片描述
    • 一个进程是可以使用管道完成读写操作的,但为了避免死锁或阻塞,通常避免在同一个进程中对管道的两端同时进行读写操作。
    • 管道的读写行为:读操作,有数据时read正常读,返回读出的字节数,没有数据时,若写端全部关闭,read解除阻塞,返回0, 相当于读文件读到了尾部,若写端没有全部关闭,read阻塞。
    • 写操作,读端全部关闭,则管道破裂,进程终止, 内核给当前进程发SIGPIPE信号。若读端没全部关闭但缓冲区写满了,write阻塞。若缓冲区没有满,则继续write。
    • 设置管道为非阻塞:默认情况下,管道的读写两端都是阻塞的。设置读或者写端为非阻塞,则可参考下列三个步骤进行:
      int flags = fcntl(fd[0], F_GETFL, 0);
      flag |= O_NONBLOCK;
      fcntl(fd[0], F_SETFL, flags);
      
    • 若是读端设置为非阻塞:写端没有关闭且管道中没有数据可读,则read返回-1,若管道中有数据可读,则read返回实际读到的字节数。写端已经关闭,管道中有数据可读,则read返回实际读到的字节数,若管道中没有数据可读,则read返回0。
    • 查看管道缓冲区大小:ulimit -a
  4. 命名管道-FIFO:FIFO是Linux基础文件类型中的一种(文件类型为p)但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。通过FIFO,不相关的进程也能交换数据。

    • 创建管道:mkfifo 管道名int mkfifo(const char *pathname, mode_t mode),参数说明和返回值可以查看man 3 mkfifo

    • 当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

    • 使用FIFO完成两个进程通信:进程A:1 创建一个fifo文件 2 调用open函数打开myfifo文件 3 调用write函数写入一个字符串如:“hello world”(其实是将数据写入到了内核缓冲区) 4 调用close函数关闭myfifo文件。

    • 进程B:1 调用open函数打开myfifo文件 2 调用read函数读取文件内容(其实就是从内核中读取数据) 3 打印显示读取的内容 4 调用close函数关闭myfifo文件。
      在这里插入图片描述

    • myfifo文件是在进程A中创建的,如果先启动进程B会报错。可以利用access 检测判断myfifo是否存在,不存在则创建。

  5. 内存映射区:存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
    在这里插入图片描述

  6. mmap函数:建立存储映射区。munmap函数:释放由mmap函数建立的存储映射区。

    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    addr: 	指定映射的起始地址, 通常设为NULL, 由系统指定     length:映射到内存的文件长度.
    prot:	映射区的保护方式, 最常用的: 读:PROT_READ 写:PROT_WRITE 读写:PROT_READ | PROT_WRITE
    flags:	映射区的特性, 可以是 MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享
    MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
    fd:由open返回的文件描述符, 代表要映射的文件
    offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射。
    成功:返回创建的映射区首地址; 失败:MAP_FAILED宏
    
    int munmap(void *addr, size_t length);
    addr:调用mmap函数成功返回的映射区首地址
    length:映射区大小(mmap函数的第二个参数)
    成功:返回0	失败:返回-1,设置errno值
    
    • mmap函数的使用:第一个参数写成NULL,第二个参数要映射的文件大小 > 0,第三个参数:PROT_READ 、PROT_WRITE,第四个参数:MAP_SHARED 或者 MAP_PRIVATE,第五个参数:打开的文件对应的文件描述符,第六个参数:4k的整数倍。
    • 注意事项:创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区。映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
    • mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。文件偏移量必须为0或者4K的整数倍。munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
      • 可以open的时候O_CREAT一个新文件来创建映射区吗? 不能 要保证文件>0。
      • 如果open时O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样? 不能 open打开文件权限要大于mmap的权限。
      • mmap映射完成之后, 文件描述符关闭,对mmap映射有没有影响?没有影响。
      • 如果文件偏移量为1000会怎样?必须是4k的整数倍。
      • 对mem越界操作会怎样?失败。
      • 如果mem++,munmap可否成功?不行。
      • mmap什么情况下会调用失败?文件大小 文件权限 文件偏移量。
      • 如果不检测mmap的返回值,会怎样?有可能失败,操作会失败。
    • mmap完成父子进程间通信:调用mmap函数创建存储映射区,返回映射区首地址ptr;调用fork函数创建子进程,子进程也拥有了映射区首地址; 父子进程可以通过映射区首地址指针ptr完成通信;调用munmap函数释放存储映射区。
      在这里插入图片描述
    • mmap完成没有血缘关系的进程间通信:两个进程都打开相同的文件,然后调用mmap函数建立存储映射区,这样两个进程共享同一个存储映射区。
    • 匿名映射:mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); 不使用文件,必须和MAP_SHARED一起使用,而且fd只能指定为-1,只能用于有血缘关系的进程间通信。查看帮助man mmap

三、信号

  1. 信号的概念:信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。信号的特点:简单;不能携带大量信息;满足某个特点条件才会产生。

  2. 信号的机制:进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后再继续执行。与硬件中断类似,但信号是软件层面上实现的中断,早期常被称为“软中断”。每个进程收到的所有信号,都是由内核负责发送的。
    在这里插入图片描述

  3. 信号的状态:信号有三种状态:产生、未决和递达。

    • 信号的产生:按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\;系统调用产生,如:kill、raise、abort ;软件条件产生,如:定时器alarm;硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误);命令产生,如:kill命令。
    • 未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
    • 递达:递送并且到达进程。
  4. 信号的处理方式:执行默认动作;忽略信号(丢弃不处理);捕捉信号(调用用户的自定义的处理函数)。

  5. 信号的特质:信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

    • 阻塞信号集中保存的都是被当前进程阻塞的信号。若当前进程收到的是阻塞信号集中的某些信号,这些信号需要暂时被阻塞,不予处理。
    • 信号产生后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态;若是信号从阻塞信号集中解除阻塞,则该信号会被处理,并从未决信号集中去除。
  6. 信号的四要素:1 信号的编号:kill -l命令可以查看当前系统有哪些信号,其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。 2 信号的名称 3 产生信号的事件 4信号的默认处理动作:Term:终止进程;Ign:忽略信号 ;Core:终止进程,生成Core文件;Stop:停止(暂停)进程;Cont:继续运行进程。

    • The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored。
    • 通过man 7 signal可以查看信号相关信息。
  7. 信号相关函数:signal函数:注册信号捕捉函数; kill函数/命令:给指定进程发送指定信号;raise函数:给当前进程发送指定信号(自己给自己发);abort函数:给自己发送异常终止信号 6) SIGABRT,并产生core文件;alarm函数:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器;setitimer函数:设置定时器(闹钟),可代替alarm函数,精度微秒us,可以实现周期定时。

    • 进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
    • alarm使用的是自然定时法,与进程状态无关,就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    定义了一个指向函数的指针类型`sighandler_t`,这个函数指针指向的函数必须返回 void 类型,并且接受一个 int 类型的参数
    signum:信号编号 	handler:信号处理函数
    
    int kill(pid_t pid, int sig);
    sig信号参数:使用宏名,因为不同操作系统信号编号可能不同,但名称一致
    pid参数:> 0: 发送信号给指定的进程; = 0: 发送信号给与调用kill函数进程属于同一进程组的所有进程
    < -1:|pid|发给对应进程组;= -1:发送给进程有权限发送的系统中所有进程
    返回值:成功:0	失败:-1,设置errno
    
    int raise(int sig);
    成功:0,失败非0raise(signo) == kill(getpid(), signo);
    
    void abort(void); 
    abort() == kill(getpid(), SIGABRT);
    
    unsigned int alarm(unsigned int seconds);
    返回0或剩余的秒数,无失败
    常用操作:取消定时器alarm(0),返回旧闹钟余下秒数
    
    int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
    成功:0	失败:-1,设置errno值
    which:指定定时方式
    自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间
    虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM  只计算进程占用cpu的时间
    运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
    new_value:struct itimerval, 负责设定timeout时间。
    itimerval.it_value: 设定第一次执行function所延迟的秒数 itimerval.it_interval: 设定以后每几秒执行function
    struct itimerval { 
        struct timerval it_interval; // 闹钟触发周期
        struct timerval it_value; // 闹钟触发时间
      }; 
      struct timeval { 
        long tv_sec; 			// 秒
        long tv_usec; 			// 微秒
     }             
    old_value: 存放旧的timeout值,一般指定为NULL
    
    
  8. 阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于未决状态的信号的集合,这两个集合存储在内核的PCB中。下面以SIGINT为例说明信号未决信号集和阻塞信号集的关系。

    • 当进程收到一个SIGINT信号,首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1
      • 如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集上该位置上的值保持为1,表示该信号处于未决状态;
      • 如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。
    • 当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。
  9. 信号集相关函数:由于信号集属于内核的一块区域,用户不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集的相关操作。信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集进行添加、删除等操作。

    int sigemptyset(sigset_t *set);
    函数说明:将某个信号集清0		 	
    函数返回值:成功:0;失败:-1,设置errno
    
    int sigfillset(sigset_t *set);
    函数说明:将某个信号集置1		  		
    函数返回值:成功:0;失败:-1,设置errno
    
    int sigaddset(sigset_t *set, int signum);	
    函数说明:将某个信号加入信号集合中
    函数返回值:成功:0;失败:-1,设置errno
    
    int sigdelset(sigset_t *set, int signum);		
    函数说明:将某信号从信号清出信号集   	
    函数返回值:成功:0;失败:-1,设置errno
    
    int sigismember(const sigset_t *set, int signum);
    函数说明:判断某个信号是否在信号集中
    函数返回值:在:1;不在:0;出错:-1,设置errno
    
    sigprocmask函数
    函数说明:用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程控制块中的信号屏蔽字(阻塞信号集)
    屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    函数返回值:成功:0;失败:-1,设置errno
    函数参数:
    how参数取值:假设当前的信号屏蔽字为mask
    	SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
    	SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
    	SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set
    	若调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
    set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。
    oldset:传出参数,保存旧的信号屏蔽字。
    
    int sigpending(sigset_t *set);	   
    函数说明:读取当前进程的未决信号集
    函数参数:set传出参数
    函数返回值:成功:0;失败:-1,设置errno
    
    

在这里插入图片描述

  1. 信号捕捉函数:signal函数,sigaction函数,注册一个信号处理函数。

    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    signum:捕捉的信号;  act:传入参数,新的处理方式; 	oldact:传出参数,旧的处理方式
    struct sigaction {
           void  (*sa_handler)(int);	// 信号处理函数
           void  (*sa_sigaction)(int, siginfo_t *, void *); //信号处理函数
           sigset_t  sa_mask; //信号处理函数执行期间需要阻塞的信号
           int      sa_flags; //通常为0,表示使用默认标识
           void     (*sa_restorer)(void);
    };
    
    
    • sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作。
    • sa_mask: 用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
    • sa_flags:通常设置为0,使用默认属性。
    • 信号处理不支持排队:在XXX信号处理函数执行期间, XXX信号是被阻塞的, 如果该信号产生了多次, 在XXX信号处理函数结束之后, 该XXX信号只被处理一次。在XXX信号处理函数执行期间,如果阻塞了YYY信号, 若YYY信号产生了多次, 当XXX信号处理函数结束后, YYY信号只会被处理一次。
    • 内核实现信号捕捉的过程:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
      在这里插入图片描述
  2. SIGCHLD信号:子进程退出后,内核会给它的父进程发送SIGCHLD信号,父进程收到这个信号后可以对子进程进行回收。使用SIGCHLD信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到SIGCHLD信号之后才去调用信号捕捉函数完成对子进程的回收,未收到SIGCHLD信号之前可以处理其他操作。

    • 产生SIGCHLD信号:子进程结束的时候;子进程收到SIGSTOP信号;当子进程停止时,收到SIGCONT信号。
    • 使用SIGCHLD信号完成对子进程的回收:有可能还未完成信号处理函数的注册三个子进程都退出了。解决办法:可以在fork之前先将SIGCHLD信号阻塞,当完成信号处理函数的注册后在解除阻塞。
    • 当SIGCHLD信号函数处理期间, SIGCHLD信号若再次产生是被阻塞的,而且若产生了多次, 则该信号只会被处理一次, 这样可能会产生僵尸进程。解决办法: 可以在信号处理函数里面使用while(1)循环回收, 这样就有可能出现捕获一次SIGCHLD信号但是回收了多个子进程的情况,从而可以避免产生僵尸进程。

四、守护进程和线程

  1. 守护进程:Daemon(精灵)进程,是Linux中的后台服务进程,不受用户登陆和注销的影响,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字,如vsftpd。

  2. 进程组和会话:进程组是一个或者多个进程的集合,每个进程都属于一个进程组,引入进程组是为了简化对进程的管理。当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组。一个会话是一个或多个进程组的集合。

    • 进程组ID等于第一个进程ID(组长进程)。如父进程创建了多个子进程,父进程和多个子进程同属于一个组,而由于父进程是进程组里的第一个进程,所以父进程就是这个组的组长, 组长ID==父进程ID。
    • 可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。
    • 只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
    • 进程组生存期:从进程组创建到最后一个进程离开。
    • 创建会话的进程不能是进程组组长,创建会话的进程成为一个进程组的组长进程,同时也成为会话的会长。需要有root权限(ubuntu不需要)。
    • 新创建的会话丢弃原有的控制终端,建立新会话时,先调用fork, 父进程终止,子进程调用setsid函数。
    • 可以使用ps ajx来查看进程组ID和会话ID,可以fork出几个子进程,然后查看进程组ID和会话ID。
      在这里插入图片描述
  3. 创建守护进程的模型共五步。

    • 第1步:fork子进程,父进程退出。子进程继承了父进程的进程组ID, 但具有一个新的进程ID,这样就保证了子进程不是一个进程组的组长ID,这保证了setsid函数的调用。
    • 第2步:子进程调用setsid函数创建新会话。调用这个函数以后该进程成为新会话的首进程,是会话的会长,成为一个新进程组的组长进程,是进程组组长,不受控制终端的影响。
    • 第3步:改变当前工作目录chdir。
    • 第4步:重设文件掩码 mode & ~umask,子进程会继承父进程的掩码,增加子进程程序操作的灵活性,umask(0000)
    • 第5步:关闭文件描述符(0,1,2),守护进程不受控制终端的影响所以可以关闭,以释放资源。
    • 第6步:执行核心工作,守护进程的核心代码逻辑。
  4. 线程:轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。进程:拥有独立的地址空间,拥有PCB,相当于独居。线程:有PCB,但没有独立的地址空间,多个线程共享进程空间,相当于合租。在Linux操作系统下:线程是最小的执行单位, 进程是最小分配资源单位,可看成是只有一个线程的进程。
    在这里插入图片描述

    • 线程是轻量级进程(light-weight process),也有PCB,实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone。从内核里看进程和线程是一样的,都有各自不同的PCB。进程可以蜕变成线程。察看指定线程的LWP号:ps –Lf pid
    • Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
    • 线程共享资源:文件描述符表;每种信号的处理方式;当前工作目录;用户ID和组ID;内存地址空间 (.text/.data/.bss/heap/共享库)。
    • 线程非共享资源:线程id;处理器现场和栈指针(内核栈);独立的栈空间(用户空间栈);errno变量;信号屏蔽字;调度优先级。
    • 线程优点:提高程序并发性;开销小;数据通信、共享数据方便。
    • 线程缺点:库函数,不稳定;gdb调试、编写困难;对信号支持不好。
  5. pthread_create函数:创建一个新线程。

    • 由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。
      int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg);
      pthread_t:传出参数,保存系统为我们分配好的线程ID,typedef unsigned long int pthread_t
      attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数
      start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束
      arg:线程主函数执行期间所使用的参数。
      成功,返回0	  失败,返回错误号
      
    • 如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行。
    • 如果主线程早于子线程退出,则子线程可能得不到执行,因为主线程退出,整个进程空间都会被回收,子线程没有了生存空间,所以也就得不到执行。
    • 线程之间(包含主线程和子线程)可以共享同一变量,包含全局变量或者非全局变量(但是非全局变量必须在其有效的生存期内)
    • 编写程序,主线程循环创建5个子线程,并让子线程判断自己是第几个子线程。最后每个子线程打印出来的值并不是想象中的值,比如都是5。
    • 原因:在创建子线程的时候使用循环因子作为参数传递给子线程,这样主线程和多个子线程就会共享变量i(变量i在main函数中定义,在整个进程都一直有效)所以在子线程看来变量i是合法的栈内存空间。由于主线程可能会在一个cpu时间片内连续创建了5个子线程,此时变量i的值变成了5,当主线程失去cpu的时间片后,子线程得到cpu的时间片,子线程访问的是变量i的内存空间的值,所以打印出来值为5。
    • 解决办法:不能使多个子线程都共享同一块内存空间,应该使每个子线程访问不同的内存空间,可以在主线程定义一个数组:int arr[5];,然后创建线程的时候分别传递不同的数组元素,这样每个子线程访问的就是互不相同的内存空间,这样就可以打印正确的值。
  6. pthread_exit函数:将单个线程退出;pthread_join函数:阻塞等待线程退出,获取线程退出状态。其作用,对应进程中的waitpid() 函数;pthread_detach函数:实现线程分离;pthread_cancel函数:杀死(取消)线程。其作用,对应进程中 kill() 函数;pthread_equal函数:比较两个线程ID是否相等。

    void pthread_exit(void *retval);
    retval表示线程退出状态,通常传NULL
    
    int pthread_join(pthread_t thread, void **retval); 
    thread:线程ID
    retval:存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址
    成功:0  失败:错误号
    
    int pthread_detach(pthread_t thread);
    成功:0  失败:错误号
    
    int pthread_cancel(pthread_t thread);
    成功:0  失败:错误号
    void pthread_testcancel(void);
    设置一个取消点
    
    int pthread_equal(pthread_t t1, pthread_t t2);
    
    • 在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。
    • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。
    • 一般先定义void *ptr; 然后pthread_join(threadid, &ptr)。
    • 线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。也可使用 pthread_create函数参2(线程属性)来设置线程分离。pthread_detach函数是在创建线程之后调用的。
    • 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。
    • 线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open等,可粗略认为一个系统调用(进入内核)即为一个取消点。还以通过调pthread_testcancel函数设置一个取消点。
  7. 线程属性:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

    • 设置线程属性分为以下步骤:
      1步:定义线程属性类型类型的变量
      pthread_attr_t  attr;2步:对线程属性变量进行初始化
      int pthread_attr_init (pthread_attr_t* attr);3步:设置线程为分离属性
      int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
      参数:
      attr: 线程属性
      detachstate:
      	PTHREAD_CREATE_DETACHED(分离)
      	PTHREAD_CREATE_JOINABLE(非分离)
      注意:这一步完成之后调用pthread_create函数创建线程,则创建出来的线程就是分离线程;
      其实上述三步就是pthread_create的第二个参数做准备工作。
      第4步:释放线程属性资源
      int pthread_attr_destroy(pthread_attr_t *attr);
      参数:线程属性
      
  8. 线程同步:线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。

    • 创建两个线程,让两个线程共享一个全局变量int number, 然后让每个线程数5000次数,看最后打印出这个number值是多少?
      • 调用usleep是为了让两个子线程能够轮流使用CPU,避免一个子线程在一个时间片内完成5000次数。
      • 对number执行++操作,使用了中间变量cur是为了尽可能的模拟cpu时间片用完而让出cpu的情况。
    • 经过多次测试最后的结果显示,有可能会出现number值少于5000*2=10000的情况。
    • 原因:假如子线程A执行完了cur++操作,还没有将cur的值赋值给number失去了cpu的执行权,子线程B得到了cpu执行权,而子线程B最后执行完了number=cur,而后失去了cpu的执行权;此时子线程A又重新得到cpu的执行权,并执行number=cur操作,这样会把线程B刚刚写回number的值被覆盖了,造成number值不符合预期的值。
    • 数据混乱的原因:资源共享(独享资源则不会);调度随机(线程操作共享资源的先后顺序不确定);线程间缺乏必要的同步机制。欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
    • 解决问题:原子操作指的是该操作要么不做,要么就完成。使用互斥锁其实是模拟原子操作。
    • 线程1访问共享资源的时候要先判断锁是否锁着,如果锁着就阻塞等待;若锁是解开的就将这把锁加锁,此时可以访问共享资源,访问完成后释放锁,这样其他线程就有机会获得锁。图中同一时刻,只能有一个线程持有该锁,只要该线程未完成操作就不释放锁。使用互斥锁之后,两个线程由并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到解决了。
  9. 互斥锁主要相关函数:pthread_mutex_init函数,初始化一个互斥锁(互斥量) —> 初值可看作1。pthread_mutex_destroy函数,销毁一个互斥锁。pthread_mutex_lock函数,对互斥所加锁,可理解为将mutex–。pthread_mutex_unlock函数,对互斥所解锁,可理解为将mutex ++。pthread_mutex_trylock函数,尝试加锁。

    • pthread_mutex_t mutex 类型:其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待,变量mutex只有两种取值1、0。
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    mutex:传出参数,调用时应传 &mutex
    attr:互斥锁属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)
    
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    mutex—互斥锁变量
    
    
    • restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改互斥量mutex的两种初始化方式:
      • 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。
        pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
      • 动态初始化:局部变量应采用动态初始化。pthread_mutex_init(&mutex, NULL)
  10. 加锁和解锁:lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。使用互斥锁可以解决两个线程数数不一致的问题。

五、线程同步

  1. 互斥锁:互斥锁的使用步骤。

    • 必须在所有操作共享资源的线程上都加上锁否则不能起到同步的效果。
      1 创建一把互斥锁,应该为一全局变量
      pthread_mutex_t mutex;
      2 在main函数中初始化互斥锁
      pthread_mutex_init(&mutex);---相当于mutex=1
      3 在代码中寻找共享资源(也称为临界区)
      pthread_mutex_lock(&mutex);  -- mutex = 0
      [临界区代码]
      pthread_mutex_unlock(&mutex); -- mutex = 1
      4 主线程释放互斥锁资源
      pthread_mutex_destroy(&mutex);
      
      
  2. 死锁:死锁并不是linux提供给用户的一种使用方法,而是由于用户使用互斥锁不当引起的一种现象。常见的死锁有两种,一种是自己锁自己,另一种是线程A拥有A锁,请求获得B锁;线程B拥有B锁,请求获得A锁,这样造成线程A和线程B都不释放自己的锁,而且还想得到对方的锁,从而产生死锁。

    • 解决死锁:让线程按照一定的顺序去访问共享资源;在访问其他锁的时候,需要先将自己的锁解开;调用pthread_mutex_trylock,如果加锁不成功会立刻返回。
  3. 读写锁:也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。读写锁非常适合于对数据结构读的次数远大于写的情况。读并行,写独占,当读写同时等待锁的时候写的优先级高。

    • 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
    • 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
    • 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。
      • 线程A加写锁成功, 线程B请求读锁:线程B阻塞
      • 线程A持有读锁, 线程B请求写锁:线程B阻塞
      • 线程A拥有读锁, 线程B请求读锁:线程B加锁成功
      • 线程A持有读锁, 然后线程B请求写锁, 然后线程C请求读锁:B阻塞,C阻塞 - 写的优先级高;A解锁,B线程加写锁成功,C继续阻塞;B解锁,C加读锁成功。
      • 线程A持有写锁, 然后线程B请求读锁, 然后线程C请求写锁:BC阻塞;A解锁,C加写锁成功,B继续阻塞;C解锁,B加读锁成功。
    • 读写锁主要操作函数。
      定义一把读写锁
      pthread_rwlock_t rwlock;
      初始化读写锁
      int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
      函数参数   rwlock-读写锁   attr-读写锁属性,传NULL为默认属性
      
      销毁读写锁
      int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);        
      加读锁
      int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);              
      尝试加读锁
      int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
      加写锁
      int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
      尝试加写锁
      int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
      解锁
      int pthread_rwlock_unlock(&pthread_rwlock_t *rwlock);
      
      
  4. 条件变量:条件本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。使用互斥量保护共享数据;使用条件变量可以使线程阻塞, 等待某个条件的发生, 当条件满足的时候解除阻塞。

    • 条件变量的两个动作:条件不满足, 阻塞线程;条件满足, 通知阻塞的线程解除阻塞, 开始工作。
    • 条件变量相关函数。
      定义一个条件变量
      pthread_cond_t  cond;
      
      int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
      函数描述:初始化条件变量
      函数参数: cond: 条件变量   attr: 条件变量属性, 通常传NULL
      函数返回值:成功返回0, 失败返回错误号
      
      int pthread_cond_destroy(pthread_cond_t *cond);
      函数描述: 销毁条件变量
      函数参数: 条件变量
      返回值: 成功返回0, 失败返回错误号
      int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
      函数描述: 条件不满足, 引起线程阻塞并解锁;条件满足, 解除线程阻塞, 并加锁
      函数参数:cond: 条件变量  mutex: 互斥锁变量
      函数返回值: 成功返回0, 失败返回错误号
      
      int pthread_cond_signal(pthread_cond_t *cond);
      函数描述: 唤醒至少一个阻塞在该条件变量上的线程
      函数参数: 条件变量
      函数返回值: 成功返回0, 失败返回错误号
      
      
    • 条件变量使用步骤。
      1 定义条件变量
      pthread_cont_t cond;
      2 初始化条件变量
      pthread_cond_init(&cond, NULL);
      3 在生成者线程中调用:
      pthread_cond_signal(&cond);
      4 在消费者线程中调用:
      pthread_cond_wait(&cond, &mutex);
      5 释放条件变量
      pthread_cond_destroy(&cond);	
      
      
  5. 生产者—消费者模型。
    在这里插入图片描述
    在这里插入图片描述

    • 上述代码中,生产者线程调用pthread_cond_signal函数会使消费者线程在pthread_cond_wait处解除阻塞。
    • 多个生成者和多个消费者程序在执行的时候core掉的原因分析:假若只有一个生产者生产了一个节点, 此时会调用pthread_cond_signal通知消费者线程, 此时若有多个消费者被唤醒了, 则最终只有1个消费者获得锁, 然后进行消费, 此时会将head置NULL, 然后其余的几个消费者线程只要有一个线程获得锁, 然后读取head的内容就会core掉。
    • 在使用条件变量的线程中, 能够引起线程的阻塞的地方有两个:一是在条件变量处引起阻塞---->这个阻塞会被pthread_cond_signal解除阻塞。二是互斥锁也会使线程引起阻塞----->其他线程解锁会使该线程解除阻塞。
  6. 信号量:信号量相当于多把锁, 可以理解为是加强版的互斥锁。

    • 信号量相关函数。
      定义信号量 
      sem_t sem;
      
      int sem_init(sem_t *sem, int pshared, unsigned int value);	
      函数描述: 初始化信号量
      函数参数:sem: 信号量变量  pshared: 0表示线程同步, 1表示进程同步  value: 最多有几个线程操作共享数据
      函数返回值:成功返回0, 失败返回-1, 并设置errno值
      int sem_wait(sem_t *sem);
      函数描述: 调用该函数一次, 相当于sem--, 当sem为0的时候, 引起阻塞
      函数参数: 信号量变量
      函数返回值: 成功返回0, 失败返回-1, 并设置errno值
      int sem_post(sem_t *sem);
      函数描述: 调用一次, 相当于sem++
      函数参数: 信号量变量
      函数返回值: 成功返回0, 失败返回-1, 并设置errno值
      int sem_trywait(sem_t *sem);
      函数描述: 尝试加锁, 若失败直接返回, 不阻塞
      函数参数: 信号量变量
      函数返回值: 成功返回0, 失败返回-1, 并设置errno值
      int sem_destroy(sem_t *sem);
      函数描述: 销毁信号量
      函数参数: 信号量变量
      函数返回值: 成功返回0, 失败返回-1, 并设置errno值
      	
      
    • 信号量使用步骤。
      1 定义信号量变量
      sem_t sem1;
      sem_t sem2;
      2 初始化信号量
      sem_init(&sem1, 0, 5);
      sem_init(&sem2, 0, 5);
      3 加锁
      sem_wait(&sem1);
      //共享资源
      sem_post(&sem2);
      	
      sem_wait(&sem2);
      //共享资源
      sem_post(&sem1);
      4 释放资源
      sem_destroy(sem1);
      sem_destroy(sem2);
      
    • 信号量实现生产者和消费者模型,互相通知。
      在这里插入图片描述
      在这里插入图片描述

总结

笔记来源:黑马程序员C语言课程

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值