嵌入式Linux C应用编程指南-进程、线程(速记版)

第九章 进程

9.1 进程与程序

9.1.1 main()函数由谁调用?

        C 语言程序总是从 main 函数开始执行,main()函数的原型是:

        int main(void) 或 int main(int argc, char *argv[])。

        操作系统下的应用程序在运行 main()函数之前需要先执行一段引导代码,最终由这段引导代码去调用应用程序的 main()函数。在链接时,由链接器将引导代码链接到我们的应用程序当中,一起构成最终的可执行文件。elf

        程序运行需要通过操作系统的加载器来实现,加载器是操作系统中的程序, 当执行程序时,加载器负责将此应用程序加载内存中去执行。

        在终端执行程序时,命令行参数由 shell 进程逐一解析。shell 进程会将参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,引导程序调用 main()函数。

9.1.2 程序如何结束?

        程序结束其实就是进程终止。进程终止分为正常终止异常终止

        正常终止 exit(),_exit(),_Exit()。异常终止abort()。

        main函数通过return返回也属于正常终止。

        注册进程终止处理函数 atexit()

        库函数 atexit() 用于注册一个进程在正常终止时要调用的函数。

#include <stdlib.h>
/* 注册一个进程正常终止时要调用的函数 */
int atexit(void (*function)(void));

        atexit() 注册的函数将在 _exit() 或者 _Exit()之前执行。

9.1.3 何为进程?

        进程是一个动态过程,代表程序的运行过程

        应用程序被加载到内存中运行,就成为了一个进程。

9.1.4 进程号

        Linux 系统下的每一个进程都有一个进程号(processID,简称 PID),进程号是一个用于唯一标识进程的正数。shell窗口可用 ps -aux 看机器上所有进程。

//ps指令查看进程  -a代表所有终端下的进程  -u显示的格式 -x没有终端的进程
ps -aux

        可通过系统调用 getpid()来获取本进程的进程号。getppid()获取父进程的进程号。

#include <sys/types.h>
#include <unistd.h>

/* 获取当前进程的进程号*/
pid_t getpid(void);

/* 获取父进程的进程号*/
pid_t getppid(void);

9.1.5 设计shell命令行程序

        shell是一个不断获取命令并新建进程执行命令的死循环。

        像 cd、export、echo 这种,属于内建命令。是只有父进程bash自己执行才有效的。

        因此穷举比较判断用户输入的命令是否是内建命令,然后决定bash自己执行还是fork子进程或者exec新进程执行。

实现思路:

        一个 XShell 进程运行起来第一步就是打印命令行提示符XShell是WINDOS平台的Shell,Bash Shell是LInux平台的Shell。

        Xshell 命令行提示符的组成是:[用户名@主机名 工作目录]$,那么我们自己 shell 的命令行提示符就可以按照 Xshell 的为模板,用户名,主机名,工作目录这些可以通过环境变量(USER,HOSTNAME,PWD)来获取。

/*获取环境变量 用户、主机、当前工作目录*/  //当前工作目录就是当前目录
const char* getUsername_Hostname_CWD()
{
  const char* name = getenv("USER");
  const char* host = getenv("HOSTNAME");
  const char* cwd = getenv("PWD");
}

第二步就是获取用户的输入 

        用户在输入的时候,会在命令与选项之间加上空格,所以不能使用 scanf 来输入。

        所以要使用 fgetsgets 来获取用户输入。

        C语言默认打开三个默认输入输出流:stdin,stdout,stderror。

        但是在输入的时候,会默认加上一个回车,使得我们输入的字符串后面会有一个‘\n’,所以需要将 '\n' 的位置改为'\0'。

        最后返回输入命令字符串的长度,方便后面判断命令字符串是否只有‘\n’。

/*获取用户输入的指令*/
int getUserCommand(char* command, int num)
{
  printf("[%s@%s %s]#", getUsername(), getHostname(), getCwd());
  char* r = fgets(command, num, stdin); //输入中间会有空格,不能用scanf
  if(r == NULL) return -1;
  // remove the '\n'
  command[strlen(command) - 1] = '\0';//输入结尾的\n换成\0
#ifdef Debug 
   printf("%s", usercommand); //test
#endif
  return strlen(command);
}

        第三步分割用户输入的命令和参数,传递给 exec 系列的接口来实现程序替换。

        在C语言中使用 strtok 函数,在C++ 中使用 string 的 substr 接口。

        strtok 函数被用来由分隔符 SEP 分隔字符串,并将分割出的子字符串存储到字符串数组 out 中。 

#define SEP " "

/*指令分割*/
void commandSplit(char* in, char* out[])
{
  int argc = 0;
  out[argc++] = strtok(in, SEP);
  while(out[argc++] = strtok(NULL, SEP));//只要out[argc++]返回的不是NULL指针
#ifdef Debug
  for(int i = 0; out[i]; i++)
  {
    printf("%d:%s\n", i, out[i]);
  }
#endif
}
/*执行指令*/
int execute(char* argv[])
{
  pid_t id = fork();
  if(id < 0) return -1;
  else if(id == 0) //子进程执行内容
  {
    execvp(argv[0], argv);//exec族函数,直接新进程运行当前程序
    exit(1);
  }
  else // father
  {
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);//父进程阻塞回收子进程
    if(rid > 0){ // wait success
      lastcode = WEXITSTATUS(status);
    }
  }
  return 0;
}

9.2 进程的环境变量

        每一个进程都有一个环境列表(environ)。将进程相关的环境变量以字符串的形式存储在一个字符串数组中。每个字符串都是以“name=value”形式定义,也就是环境变量的键值对集合。

        在 shell 终端下可以使用 env 命令查看到 shell 进程的所有环境变量,

        使用 export 命令可以添加环境变量。使用 export -n 命令可以删除环境变量。

export LINUX_APP=123456 # 添加 LINUX_APP 环境变量
export -n LINUX_APP # 删除 LINUX_APP 环境变量

9.2.1 应用程序获取环境变量

        进程的环境变量是从父进程继承过来的。

        在 shell 终端下执行一个应用程序,该进程的环境变量就是从父进程(shell 进程)继承过来的。

        环境变量键值对的形式存放在环境列表中,通过全局变量 environ 指向环境列表。在应用程序中使用环境变量 environ 需要extern 声明。

extern char **environ; // 申明外部全局变量 environ

        获取指定环境变量 getenv()

        库函数 getenv() 用于获取指定环境变量。

#include <stdlib.h>

/*通过环境变量名获取指定环境变量*/
char *getenv(const char *name);

9.2.2 添加/删除/修改环境变量

        库函数

        putenv()        添加环境变量

        setenv()        修改环境变量

        unsetenv()    删除环境变量

        clearenv()     清空环境变量

#include <stdlib.h>

/*向环境列表environ添加环境变量,字符串,name-value键值对*/
int putenv(char *string);

/*根据环境变量名修改环境变量,可设置若环境变量已经存在是否覆盖*/
int setenv(const char *name,//环境变量名
           const char *value,//环境变量值
           int overwrite);//0/非0,是否覆盖。

/* 删除环境变量 */
int unsetenv(const char *name);

/* 清空环境变量。等效于environ=NULL*/
int clearenv(void);

9.2.3 环境变量的作用

        在 shell 中,每一个环境变量都有它所表示的含义。

        比如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变 量表示当前所在目录等。

9.3 进程的内存布局

        C语言,从古至今,都是代码段、数据段、BSS段、堆、栈。C++多个mmap映射区

        代码段放代码。源代码经过预处理、编译、汇编、链接,得到可执行文件。可执行文件要读到内存中去执行。

        数据段,放初始化好了的全局变量和静态变量

        BSS段,放没显式初始化的全局变量和静态变量。BSS段的内存一般在程序执行前初始化为0。BSS段都是符号引用,可执行文件只记录BSS段的位置和整体大小,运行时由加载器分配空间。

        ,一块可在运行时动态分配的内存空间。malloc,calloc动态分配。

        ,函数的参数、局部变量、返回值、入口地址都放在栈上。每个函数有自己的栈帧。 

         size命令可查看可执行文件的代码段、数据段、BSS段的大小。

size 可执行文件名

9.4 进程的虚拟地址空间

        大多数操作系统都采用了虚拟内存管理技术。

        每一个进程都在自己独立的地址空间中运行。

        在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程占 3G,内核独自占 1G

         虚拟地址会通过 MMU(内存管理单元) 映射到实际的物理地址空间。对虚拟地址的读写操作实际上就是对物理地址的读写操作。

         虚拟地址空间的引入,最大的好处在于:

        对物理地址空间的隔离。进程和进程只能操作自己的空间。

        便于确定程序的链接地址。因为程序加载到内存的哪块地址是随机的。

        便于内存共享。修改映射规则就能共享内存。

        便于内存保护机制。不同线程对内存有不同权限。

9.5 fork()创建子进程

        系统调用 fork()函数会复制函数后面的代码,创建子进程去执行。

#include <unistd.h> 

/*复制函数后续代码,创建子进程去执行。成功返回0,失败返回-1。*/
pid_t fork(void);

        创建多个进程是任务分解时的方法。比如网络服务器在监听客户端请求的同时,创建子进程去处理其他请求事件。

        子进程拷贝父进程的数据段、堆、栈以及父进程打开文件描述符,父进程与子进程并不共享这些存储空间,子进程是一个完全独立的进程,有自己的 PID 和 PCB。

        在调用了fork()之后,父、子进程一般只有一个会通过调用 exit() 退出进程,而另一个则应该使用 _exit() 退出

9.6 父、子进程间的文件共享

        调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,类似于 dup()。

        这意味着父子进程各自 PCB文件描述符表中的文件描述符,均指向相同的文件表。这意味着子进程更改了继承的文件的偏移量,父进程也会受到影响。

9.7 系统调用 vfork()

        vfork()与 fork()函数在功能上是相同的,并且都返回子线程的PID。

#include <sys/types.h>
#include <unistd.h>

/* 采用写时复制技术的 子进程复制,是为 子进程立刻执行 exec() 设计的 */
pid_t vfork(void);

        fork() 复制了父进程的数据段、堆栈、打开的文件描述符,消耗较大;而且子进程如果调用了exec() 将会执行新程序的main代码段,并为新程序成功重新初始化数据段、堆栈。导致浪费。

        内核采用写时复制技术来避免这种浪费。写时复制,copy-on-write。

        vfork() 是为了 子函数创建后立刻执行exec() 设计的。

        vfork()与 fork()一样用来创建子进程,在子进程 调用 exec 或_exit 之前,它在父进程的空间中运行、子进程共享父进程的内存。但子进程修改了父进程的数据可能带来未知的结果。

        vfork()保证子进程先运行,子进程调用 exec() 之后父进程才可能被调度运行。

        一般应在子进程中立即调用 exec,如果 exec 调用失败,子进程则应调用_exit() 退出。

        vfork 产生的子进程不应调用 exit 退出,因为这会导致对父进程 stdio 缓冲区的刷新和关闭,会导致注册的退出清理程序的执行

9.8 fork()之后的竞争条件

        调用 fork()之后,子进程成为了一个独立的进程,CPU自由调度,运行顺序是不确定的。

        如果要确定先后顺序,可使用 sleep休眠程序,然后另一个给进程通过 kill发送信号唤醒。

9.9 进程的诞生与终止

9.9.1 进程的诞生

        使用"ps -aux"命令可以查看到系统下所有进程信息。

        进程号为1的 init进程 被称为守护进程。是所有进程的父进程。

        fork一个子进程时,新的进程便诞生。

9.9.2 进程的终止

        进程有两种终止方式:异常终止正常终止

        进程的正常终止有多种不同的方式,譬如在 main 函数中使用 return 返回、调用 exit()函数结束进程、 调用_exit()或_Exit()函数结束进程等。

        异常终止通常也有多种不同的方式,譬如在程序当中调用 abort()函数异常终止进程、当进程接收到某些信号导致异常终止等。

        _exit()函数和 exit()函数的 status 参数定义了进程的终止状态,父进程可以调用 wait() 函数以获取该状态。虽然参数 status 定义为 int 类型,但仅有低 8 位表示它的终止状态,一般来说,终止状 态为 0 表示进程成功终止,而非 0 值则表示进程在执行过程中出现了一些错误而终止,譬如文件打开失败、 读写失败等等,对非 0 返回值的解析并无定例。

        当子进程结束时,‌它会向父进程发送一个 SIGCHLD 信号。‌

        wait(&statue)函数可以用来阻塞父进程,等待子进程的 SIGCHLD信号,并获得子进程的结束状态。

         一般使用 exit()库函数而非_exit()系统调用,原因在于 exit()最终也会通过 _exit()终止进程。

        exit() 的任务包括:

        1、调用进程终止处理函数

        2、关闭进程的stdio流缓冲区

        3、执行 _exit()系统调用来关闭文件描述符,回收堆栈等数据结构。

         vfork 创建的父子进程只有一个能使用exit()退出,推荐父进程,另一个应使用 _exit。避免stdio流缓冲关闭。

9.10 回收子进程

9.10.1 wait()函数

        系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息。子进程终止时会给父进程发送 SIGCHLD信号

#include <sys/types.h>
#include <sys/wait.h>

/*等待子进程的SIGCHLD信号,并获取子进程终止状态*/
pid_t wait(int *status); //如果没有子进程,会返回-1

        可使用以下宏来检查status参数。

WIFEXITED(status):子进程正常终止,返回 true;
WEXITSTATUS(status):返回子进程调用_exit()或exit()时指定的退出状态
WIFSIGNALED(status):子进程被信号终止,则返回 true;
WTERMSIG(status):返回导致子进程终止的信号编号
WCOREDUMP(status):子进程终止时产生了核心转储文件,则返回 true;

9.10.2 waitpid()函数

        wait() 系统调用只能阻塞式等待子程序终止,且任意子程序的 SIGCHLD信号都会导致唤醒。

        waitpid() 系统调用可以等待特定PID的进程终止。而且是非阻塞式等待。

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid,    //进程id
              int *status,  //子进程终止状态
              int options); //等待操作

/*
    pid>0,代表等待进程号为pid的子进程。
    pid=0,代表同进程组所有子进程。
    pid=-1,代表等待任意子进程。
    pid<-1,等待通组与pid绝对值相等的进程
*/

/*
等待操作:
    WNOHANG    //子进程没有终止或者暂停,则立即返回.即非阻塞等待。
    WUNTRACED  //除了返回终止的子进程状态信息,还返回因信号而停止的子进程状态信息。
    WCONTINUED //返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
*/

9.10.3 僵尸进程与孤儿进程

        父进程结束,子进程还在。孤儿。

        子进程没了,父进程还在,父进程来不及收尸。僵尸。

        子进程结束后,父进程应该调用 wait()/waitpid() 给子进程收尸。父进程处理了子进程的SIGCHLD信号以后,子进程就被内核彻底删除,PID回收。

        如果父进程没有调用 wait() 就结束了,init 进程会自动接管子进程,并调用 wait() 来回收子进程。

        ubuntu图形化界面的孤儿会被图形化界面的守护进程收养,爹没了,upstart 守护进程 就是爹,而不是 进程号为1的 Init守护进程。Ctrl + Alt + F1可以进入Ubuntu的字符界面。字符界面无法显示中文。

9.11 执行新程序 exec族函数

        当子进程的工作不是运行父进程的代码段,而是运行一个新程序的代码,那么这个时候子进程就可以通过 exec 族函数来运行新的 main。

9.11.1 execve()函数

        系统调用 execve() 将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,栈、堆、数据都会替换,然后从新程序的 main()函数开始执行。

#include <unistd.h>

/* 将外部可执行文件载入内存。永不返回,有返回值就代表新进程运行出现错误。 */
int execve(const char *filename,//文件路径
           char *const argv[],  //参数 
           char *const envp[]); //环境列表

        基于系统调用 execve(),还提供了一系列以 exec 为前缀命名的库函数,称为 exec 族函数。

        通过 exec 族函数加载一个外部新程序的过程称为 exec 操作

9.11.2 exec 库函数

        execve属于系统调用。exec 库函数都是基于 execve实现的。它们参数各异但功能相同。

9.11.3 system()函数

        system() 函数用来在程序当中执行 shell命令

#include <stdlib.h>

int system(const char *command);

        system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能。

        system() 会调用 fork()创建一个子进程并执行exec操作来运行 shell进程,然后执行参数 command 命令。并通过waitpid() 监视shell进程的运行状态。

9.12 进程状态与进程关系

9.12.1 进程状态

        进程有 6 种不同的状态,分为:

        就绪

        运行

        僵尸

        可中断睡眠状态(浅度睡眠)

        不可中断睡眠状态(深度睡眠)

        暂停

        就绪态(Ready):处于就绪态链表,等CPU调度。

        运行态:正在被CPU调度。

        僵尸态:父进程挂了,没有被wait()回收。等init守护进程接管。

        可中断睡眠态:可被中断、信号唤醒。

        不可中断睡眠态:进程阻塞,只能等特定条件唤醒。

        暂停态:一般可通过信号将进程暂停,譬如 SIGSTOP 信号暂停;譬如收到 SIGCONT 信号从暂停恢复到就绪。

9.12.2 进程关系

        getgpid()        //获取进程的组ID

        setgpid()        //设置/创建进程的组ID

        进程有自己的PID,init 守护进程是所有进程的父进程。

        进程间存在着多种不同的关系,包括:无关系父子关系进程组会话

        进程组

        每个进程除了有 PID之外,还有一个组ID(GPID)。比如要同时终止100个进程,就可为进程分组,通过组ID进行批量操作。

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

        在组ID前加上符号,就代表控制整个组的进程

        一个组长进程只能管理一个组。

        只要进程组中还存在一个进程,该进程组就存在,与组长进程是否终止无关。

        新创建的进程会继承父进程的进程组ID。

        系统调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID。

#include <unistd.h> 

/* 获取进程的进程组ID */
pid_t getpgid(pid_t pid); 

/* 获取进程的进程组ID ,等价于 getpgid(0)*/
pid_t getpgrp(void); //参数0代表获取调用进程的组ID

        系统调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组。

#include <unistd.h>
/* 设置进程的GPID */
int setpgid(pid_t pid, //进程PID。0代表使用者进程的pid
            pid_t pgid);//组PID。0代表作为组长新建进程组。如果gid=pid则指定pid为组长进程。

int setpgrp(void);//等价于setpgid(0,0),创建新进程组,当前进程设置为组长

        会话

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

会话多个进程组首领进程、控制终端、前台进程组后台进程组。 

getsid()  //获取会话id

setsid()  //用当前进程创建会话

        一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一 个会话首领(leader),即创建会话的进程

        控制终端可有可无,在有控制终端的情况下也只能连接一个控制终端,通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备 (譬如通过 SSH 协议网络登录)。

        一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程组

        会话的首领进程连接一个终端之后,该终端就成为会话的控制终端

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

        产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程,譬如 Ctrl + C(产 生 SIGINT 信号)、Ctrl + Z(产生 SIGTSTP 信号)、Ctrl + \(产生 SIGQUIT 信号)等等这些由控制终端产生的信号。

        当用户在某个终端登录时,一个新的会话就开始了。Linux系统下打开多个终端窗口,其实就是创建了多个会话。

        会话中首领进程的组ID,就是会话的会话ID(sid)

        系统调用 getsid()可以获取进程的会话 ID

#include <unistd.h>

/*获取进程的会话ID(sid)*/
pid_t getsid(pid_t pid);

         系统调用 setsid()可以直接使用当前进程创建一个会话。如果当前进程不是进程组的组长,就会新建一个进程组。当前线程就是新建会话的首领线程。

#include <unistd.h>

/*创建一个会话*/
pid_t setsid(void);

9.13 守护进程(deamon涤门)

9.13.1 何为守护进程

        守护进程也成为精灵进程。独立于进程终端

        shell终端就是一个典型的会话控制终端,从终端运行的程序都属于终端的子进程。一切进程都是进程号PID为1的守护进程(init)的子进程。

        终端一挂,会话退出,终端下的所有进程按道理来讲就会成为孤儿进程,这时候init守护进程就会接管,调用waitpid()终止这些进程。

        守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。

        守护进程自成进程组、自成会话。

        命令 ps -ajx 查看系统所有进程。 

ps -ajx

a:‌显示所有终端下的进程,‌包括其他用户的进程。‌
j:‌采用作业控制的格式显示进程信息。‌包括进程组ID(‌PGID)‌和会话ID(‌SID)‌
x:‌显示没有控制终端的进程。‌包括后台进程和系统进程。‌

        守护进程可以通过终端命令行启动,但通常它们是由系统初始化脚本进行启动,譬如/etc/rc*或 /etc/init.d/*等。 

9.13.2 创建守护进程

        让进程调用 setsid() 创建会话

        让进程的工作目录是根目录。因为工作目录所在的文件系统不能卸载,根目录方便。

        使用umask()修改文件权限掩码

        关闭不用的文件描述符

        将守护进程的标准输入、标准输出、标准错误重定向到/dev/null。守护进程不需要打印也不需要用户交互。

9.14.3 SIGHUP 信号

        当用户退出会话时,系统向该会话中所有子进程发出 SIGHUP 信号。

        子进程接收到 SIGHUP 信号后自动终止,会话中的所有进程都退出时,会话也就终止了。

        进程如果设置信号掩码umask忽略SIGHUP信号,终端关闭后,其他进程都挂掉,该进程也不受SIGHUP信号影响,直接就变成守护进程。

9.14 单例模式运行

        单例模式限制一个程序只被运行一次,不允许多个进程运行一样的程序。

9.14.1 通过文件存在与否进行判断

        程序运行前判断自己创建的文件是否存在,存在就代表其他程序在运行。掉电关机,文件没删除remove()成,就废了。

9.14.2 使用文件锁

        通过系统调用 flock()、或库函数 lockf()均可实现对文件进行上锁。

         进程退出文件锁自动释放。

第十章 进程间通信

10.1 进程间通信简介

        进程间通信(interprocess communication,简称 IPC)指两个进程之间的通信。

        系统中的每一个进程都有 各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。

10.2 进程间通信的机制有哪些?

管道消息队列信号量共享内存

        进程间通信记住 System V IPC。线程间通信记住 POSIX

System V IPC:信号量、消息队列、共享内存;

Socket IPC:基于 Socket 进程间通信。 

10.3 管道和 FIFO

        把一个进程连接到另一个进程的数据流称为管道,管道被抽象成一个文件。

管道包括两种:

        无名管道 pipe半双工,数据只能单向传输。只能在父子、兄弟进程间使用;

        有名管道 name_pipe(FIFO)全双工。允许在不相关的进程间进行通讯。

10.3.1 无名管道

        无名管道不存在于文件系统中,但是存放在内存中,实质上是内核缓冲区,无法使用open来获取无名管道的文件描述符。

管道操作符 " | "

        常见的形态就是我们在 shell 操作的 |  | 被称为管道操作符。用于将一个命令的输出直接作为另一个命令的输入,‌实现命令之间的数据传递。‌

ps -aux | grep root

        ps用来查看进程,-a代表当前窗口下所有进程,-u代表用户格式显示,-x代表其他窗口下的所有进程。管道操作符  |  用于让左边进程的输出,流向右边进程的输入。

        grep 使用正则表达式搜索文本,查找出包含 "root" 字样的行。

        这里就相当于打开了两个进程,一个查询进程,一个搜索匹配的字符。

        无名管道pipe

        使用 pipe() 创建无名管道。

        使用 write() 或者read() / select() 读写文件描述符。不需要open打开。

        使用 close() 手动关闭文件描述符。

#include <unistd.h>

/* 创建无名管道 */
int pipe(int filedes[2]);//读/写文件描述符

        无名管道pipe的本质是一个内核缓冲区。

        数据从写端流入,从读端流出。被抽象成读/写两个文件描述符

        读的时候可以用 read读,但是read会阻塞。

        也可以用 select 去轮询描述符列表fd_sets,设置等待时间来非阻塞。

         select会轮询监视的读/写/异常文件集合,返回就绪的文件数。

int select(int nfds,      //监控的文件描述符集合中最大文件描述符的值加1
           fd_set *readfds,//读监视文件集合
           fd_set *writefds, //写监视文件集合
           fd_set *exceptfds, //异常监视文件集合
           struct timeval *timeout);//超时

10.3.2 有名管道FIFO

        命名管道FIFO用于不相关文件之间通信

        FIFO和PIPE一样是伪文件,但是FIFO 在文件系统中以文件名的形式存在,虽然也存放在内存中,但是可以使用唯一路径名访问。

        可以使用 mkfifo 指定路径和读写权限来创建有名管道。

int mkfifo(const char * pathname,//有名管道路径
           mode_t mode);         //
/*
O_RDONLY:读管道。
O_WRONLY:写管道。
O_RDWR:读写管道。
O_NONBLOCK:非阻塞。
O_CREAT:文件不存在就创建新文件,并设置为读写权限。
O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。
*/

FIFO 常用来作Linux日志。因为各个进程毫无关联,无法用互斥锁、信号量等确保文件安全。

FIFO 的写入具有原子性,可以保证不同进程操作数据的完整且不错乱。

10.4 信号

        信号是事件发生时对进程的通知机制,也称为软件中断

        信号可以用于进程间通信,也可以发送信号给进程本身。

       Ctrl + c:终止信号 Abort。异常退出。
       Ctrl + \:退出信号 exit。执行清理。
       Ctrl + z:停止信号 stop。阻塞进程。

信号基础

内核对每个信号都定义了唯一的信号编号,从数字 1 开始顺序展开。

每个信号都有一个宏作为信号的名字。信号宏名字与信号编号对应。

每个信号的宏名字都是以 SIGxxx 开头。

信号分为可靠信号、不可靠信号;实时信号、非实时信号

        实时信号都是可靠信号,都支持排队。

信号发送和处理 

有信号发送函数 sigqueue() 和 信号处理绑定函数 sigaction()/signal()。

非实时信号是不可靠信号,不支持排队。有信号发送函数 kill()。raise()向自身发送信号。

非实时信号又被称为标准信号。信号编号为1~31。

信号集

sigset_t是信号集,存放多个信号。

sigemptyset() 初始化向信号集填空。

sigfullset() 初始化向信号集填全部信号。

sigaddset() 向信号集添加信号。

sigdelset() 向信号集删除信号。

sigismember() 判断信号是否在信号集中。

获取信号的描述信息

sys_siglist[ ]数组。

strsignal() 函数。

psignal()函数。向标准错误输出信号描述信息。

信号掩码

        信号掩码就是一个信号集。

        掩码中的信号不会被内核传递给进程,但是仍然存在于等待信号集中,也就是直到被从掩码中移出了才做处理

        调用 sigaction()/signal() 函数为某一个信号设置处理方式后,在处理该信号的过程中会将该信号添加到信号掩码,防止信号执行期间被同类信号中断

系统调用 sigprocmask(),可以自由使用新的掩码集添加、替换、移除信号掩码

阻塞等待信号

        sigsuspend() 可以原子操作参数掩码集替换替换进程当前掩码,并pause()挂起进程等待信号,被唤醒后恢复掩码集。相当于原子封装了sigpromask、pause、sigpromask。

//pause会挂起进程,直到信号到来

实时信号

         如果进程当前正在执行信号处理函数,新来的信号是进程信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集

        非实时信号执行期间来了多次只能算一次,实时信号执行期间来几次算几次。

sigpending() 用于获取等待信号集中处于等待状态的信号。

        发送实时信号可指定伴随数据,不同实时信号按优先级排序。

发送进程使用系统调用 sigqueue()向另一个进程发送实时信号以及伴随数据

        接收进程使用sigaction()为信号建立处理函数, 并加入 SA_SIGINFO 作为处理信号的标志。也就是要使用 sa_sigaction 指针指向的处理函数,才能获取信号的伴随数据。

10.5 消息队列

10.5.1 system-V IPC 和 Posix

        进程间 消息队列共享内存、信号量常用 System-V IPC标准。由于支持进程间通信,因此属于有名信号量。

        IPC对象都使用一种叫做 key 的键值来作唯一标识。

        POSIX 提供的信号量既能支持进程间同步,也能支持线程间同步。同时提供了 类似文件操作的有名信号量 和 内存数据操作的无名信号量信号量名为/+其他字符

10.5.2 ftok() 函数

        ftok函数创建IPC的键值。

//创建 IPC对象的键值key。结合路径名与项目ID生成。
key_t ftok(const char *pathname,//路径名
           int proj_id);        //项目ID

        IPC对象都是持续性资源,被创建之后不会因为进程的退出而消失,除非调用特殊的函数或者命令删除他们。

        Linux的IPC对象(包括消息队列、共享内存和信号量)在内核内部使用链表维护,通过ipcs 命令可以查看系统当前的IPC对象

ipcs指令查看IPC对象

10.5.3 消息结构体 msgbuf

         消息队列提供一种进程间互相发送数据块的方法。

        与信号相比,消息队列发送的信息量更大;与有名管道FIFO相比,消息队列独立于发送和接收进程存在

        消息队列的消息是一个结构体,由消息类型和数据组成。消息类型必须>0。使用函数发送/接收消息时函数参数的消息大小计算时不计入消息类型的大小。

struct msgbuf {
	long mtype;         /*消息类型,必须>0*/
	char mtext[1];       /*消息数据*/
};

        消息队列的操作有 4 种包括:

        创建/打开消息队列  msgget()  //传入IPC键值和操作模式。不存在则创建,存在则报错

        发送消息                 msgsnd()  //给 msgID 发送消息。指定缓冲区、数据大小、模式。

        接收消息                 msgrcv()  //从 msgID 接收消息。指定缓冲区、数据个数、

        控制消息                 msgctl()   //

10.5.4 msgget 函数

        打开消息队列,不存在则创建。可以像打开文件一样指定操作模式,使得打开时不存在则创建,存在则报错。

        打开或创建成功返回消息队列标识ID,msqid

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

/* 创建/打开消息队列,返回消息队列的标识符ID */
int msgget(key_t key, //IPC键值
           int msgflg);//消息队列标志
/*
IPC_CREAT : 消息队列不存在则创建,否则打开;
IPC_EXCL  : 不存在则创建之,存在产生一个错误并返回。和IPC_CREAT 一起用。
*/

10.5.5 msgsnd 函数

         向指定消息队列ID发送指定大小的消息。可指定行为包括满时阻塞满时返回错误码消息过大则截断

/* 发送消息 */
int msgsnd(int msqid,        //消息队列的ID
           const void *msgp, //消息指针。可以是任何结构体,但第一个字段必须为long类型,表明发送此消息的类型
           size_t msgsz,     //消息的大小
           int msgflg);      //消息队列行为标志
/*
向消息队列ID发送消息的模式:
    0           满时阻塞
    IPC_NOWAIT  满时返回错误码
    IPC_NOERROR 发送消息大于消息队列的size,消息则被截断,且不报错
*/

10.5.6 msgrcv 函数

        用来接收消息队列的消息。可以通过 msgbuf 的消息类型msgtype,来表示要接收的是消息队列的第一条消息与msgtype相等的第一条消息、还是绝对值小于等于msgtype的第一条消息

        接受的模式包括阻塞式接收非阻塞式接收满足条件消息的size过大则截断

/* 接收消息*/ 
ssize_t msgrcv(int msqid,  //消息队列标识ID
               void *msgp, //存放消息的消息结构体缓冲区指针
               size_t msgsz,//消息大小
               long msgtyp, //消息类型  0接收第一个消息,
                            //         >0接收类型等于msgtyp的第一个消息,
                            //         <0接收类型绝对值等于或小于该值的第一个消息
               int msgflg); //接收操作的类型标志
/*
接收消息操作:
    0             阻塞式接收
    IPC_NOWAIT    非阻塞式,直接返回错误
    IPC_EXCEPT    与msgtype配合使用返回队列中第一个类型不为msgtype的消息    
    IPC_NOERROR   如果队列中满足条件的消息大于请求的size,截断
    
*/

10.5.7 msgctl 函数

        控制消息。用来获取、设置消息队列的属性删除消息队列

        可设置的属性包括:消息队列的uid、gid、读写权限、消息队列的最大字节。

/* 控制消息。用来获取和设置消息队列的属性*/
int msgctl(int msqid, //msg标识id
           int cmd,   //IPC_SET  设置消息队列的状态
                      //IPC_STAT 获得消息队列的状态
                      //IPC_RMID 删除消息队列
           struct msqid_ds *buf);//消息队列管理结构体

//可设置的属性包括:消息队列的uid、gid、读写权限、消息队列的最大字节。
    IPC_SET  设置消息队列的状态
    IPC_STAT 获得消息队列的状态
    IPC_RMID 删除消息队列

10.6 信号量

10.6.1 概念

        进程的信号量分为命名信号量无名信号量。命名信号量可以跨进程通信,无名信号量只能在同一进程内使用

        Linux内核为每个信号量集维护了一个semid_ds数据结构示例。该结构定义在头文件linux/sem.h中。

        信号量集是一个数组,一个信号量集有一个信号量ID,信号量集中的信号量按照数组元素进行编号。

struct semid_ds {
	struct ipc_perm	sem_perm;		/* 对信号进行操作的许可权,和上一节的消息队列一样的 */
	__kernel_old_time_t sem_otime;		/* 对信号量进行操作的最后时间 */
	__kernel_old_time_t sem_ctime;		/* 对信号量进行修改的最后时间 */
	struct sem	*sem_base;		/* 指向第一个信号量 */
	struct sem_queue *sem_pending;		/* 等待处理的挂起操作 */
	struct sem_queue **sem_pending_last;	/* 最后一个正在挂起的操作 */
	struct sem_undo	*undo;			/* 撤销的请求 */
	unsigned short	sem_nsems;		/* 数组中的信号量个数 */
};

10.6.2 特点

        信号量是计数器,用于实现进程间的互斥与同步,若要实现数据传递需要结合共享内存

        信号量是基于操作系统的PV操作。程序对信号量的操作都是原子操作

信号量类型 sem_t

PV

操作

        一种实现进程互斥与同步的有效方法。

PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思

原子操作        指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换

        信号量和消息队列都是持久性资源,需要手动关闭,不然进程结束了还在。

10.6.3 PV操作

P操作:P操作表示请求(Pass),用于测试信号量的值,并将信号量减1。如果能拿到信号量,就继续执行,否则阻塞并进入等待队列

V操作:V操作表示释放(Vrijgeven),将信号量加1。如果原本信号量就够用,则不管,如果原本信号量就是负数了,代表阻塞队列有进程在等,就唤醒一个进程,然后继续执行。

        二值信号量只能取0和1,通用信号量指的是能取多个正整数的信号量。

10.6.4 semget 函数

        创建或获取一个信号量组:若成功返回信号量标识符ID,失败返回-1。

#include<semphore.h>

int semget(key_t key,//所创建或打开信号量集的键值
           int nsems,//信号量的个数,如果打开一个现有的信号量集,则给0
           int semflg);//信号量集的访问权限和函数的操作类型。
                       //可读/可写/不存在则创建/存在则报错|权限

//例子 semid = semget(key,1,IPC_CREAT|0666);//以可读可写权限创建信号量

10.6.5 semop 函数

        对信号量组中给定信号量标识符ID信号量编号进行添加信号量减少信号量操作,其实就是PV操作,改变信号量的值。成功返回0,失败返回-1。

/* 信号量操作 */
int semop(int semid,     //信号量标识符ID
          struct sembuf *sops,//信号操作结构体指针。
          unsigned nsops);//信号操作结构的数量,恒为1
/* 信号操作结构体 */
struct sembuf
{
    short sem_num; // 信号量在信号量集中的索引,默认为0
    short sem_op;  // 一个是-1,即P(等待)操作,
                   // 一个是+1,即V(发送信号)操作。
    short sem_flg; // SEM_UNDO,进程在没有释放信号量的清空下终止,则撤销进程上次操作(常用)
                   // IPC_NOWAIT,如果操作导致进程阻塞,则不操作
};

10.6.6 semctl 函数

        给出指令,控制指定信号量标识符信号量编号的信号量的相关信息。

        指令包括设置信号量的值删除信号量组等。

int semctl(int semid,  //信号量标识符ID
           int semnum, //信号量在信号集中的编号。0代表第一个。
           int cmd, //指令
           ...);//semum结构体,用来传值

/*
操作指令:
    SETVAL   初始化信号量为一个已知的值。
    IPC_RMID 删除信号量集合
    IPC_STAT 用于获取信号量的当前状态
    IPC_SET  设置信号量的某些属性(如权限)
    GETALL   获取所有信号量的值
    SETALL   设置所有信号量的值
    IPC_INFO 获取信号量集的相关信息,如最大信号量总数,现有信号量个数
*/
union semun
{
	int val;    /* 用来设置、获取信号值*/
	struct semid_ds *buf;    /* IPC_STAT, IPC_SET 的 Buffer */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO*/
};

10.7 共享内存

        共享内存是分配一块能被其他进程访问的内存。每个共享内存段在内核中维护一个内部数据结构shmid_ds(和消息队列、信号量一样),该结构定义在头文件linux/shm.h中。sys/shm.h同样定义了该结构体,并提供了创建与操作的函数。

        对共享内存的连接使得不同进程可以操作同一块内存。多进程下一般使用信号量上锁解锁。

#include<linux/shm.h>
struct shmid_ds {
	struct ipc_perm		shm_perm;	/* 操作许可 */
	int			shm_segsz;	/* size of segment (bytes) */
	__kernel_old_time_t	shm_atime;	/* 最后一个进程访问共享内存的时间 */
	__kernel_old_time_t	shm_dtime;	/* 最后一个进程离开共享内存的时间 */
	__kernel_old_time_t	shm_ctime;	/* last change time */
	__kernel_ipc_pid_t	shm_cpid;	/* pid of creator */
	__kernel_ipc_pid_t	shm_lpid;	/* pid of last operator */
	unsigned short		shm_nattch;	/* 当前使用该共享内存段的进程数量 */
	unsigned short 		shm_unused;	/* compatibility */
	void 			*shm_unused2;	/* ditto - used by DIPC */
	void			*shm_unused3;	/* unused */
};

10.7.1 shmget 函数

        使用函数shmget创建或者访问一个共享内存区

#include <sys/ipc.h>  
#include <sys/shm.h>  

/*创建/打开 共享内存*/
int shmget(key_t key, //键值
           size_t size,//内存大小
           int shmflg);//访问和操作权限。和文件一样。

10.7.2 shmat 函数

        通过共享内存区ID映射起始地址共享内存区映射到调用进程的地址空间

share memory attach

#include <sys/types.h>  
#include <sys/shm.h>  

/*将共享内存区对象映射到调用进程的地址空间*/
void *shmat(int shmid,          //共享内存区id
            const void *shmaddr,//映射起始地址。NULL为自动选择
            int shmflg);        //连接共享内存的选项。通常给0。

10.7.3 shmdt 函数

       shmdt 用于断开与共享内存的连接。一个进程结束后,与共享内存区的连接数自动减一,到零后共享内存区自动回收。

#include <sys/types.h>  
#include <sys/shm.h>  
  
/*断开与共享内存的连接*/
int shmdt(const void *shmaddr);//映射起始地址

10.7.4 shmctl 

        shmctl 用来通过指令宏设置获取删除共享内存区的状态信息。

#include <sys/types.h>  
#include <sys/shm.h>  
  
int shmctl(int shmid,//共享内存ID 
           int cmd,  //控制指令
           struct shmid_ds *buf);//指向shmid_ds结构的指针,用于传递或获取相关的信息。
/*
常用指令包括:
    IPC_STAT:获取共享内存区的状态信息,并将其存储在 buf 中。
    IPC_SET:设置共享内存区的状态信息,buf 中包含了要设置的信息。
    IPC_RMID:删除共享内存区。
*/

第十一章 线程

11.1 线程概述

        线程相关库函数用的POSIX标准

        进程有进程控制块PCB,线程有线程控制块TCB

        一个进程又是由多个线程所组成的,线程是程序中的一个执行流。

        每个线程都有自己的PC指针和SP指针,但代码区共享,即不同的线程可以执行同样的函数。

11.1.1 线程概念

        什么是线程?

        线程是系统调度的最小单位。线程属于进程管理。一个线程指一个控制流程。

        线程是如何创建起来的?

        当一个程序启动时,Init进程完成初始化操作,fork出许多子进程。main函数作为主线程开始运行。任何一个进程都包含一个主线程。

        主线程负责创建、回收子线程。

        线程的特点?

        进程是一个容器,包含了线程运行所需的数据结构、环境变量等信息。

        进程中的线程将共享该进程的全部系统资源,如虚拟地址空间文件描述符信号处理

        多进程和多线程两种编程模型的优势和劣势

        进程切换开销远大于线程切换的开销,对于一些中小型应用程序来说不划算。

        进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦。

        解决方案便是使用多线程编程,多线程能够弥补上面的问题:

        同一进程的多个线程间切换开销比较小。

        同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间中,通信容易。

        线程创建的速度远大于进程创建的速度。

        多线程在多核处理器上更有优势。但也存在编程、调试难度高,线程安全问题。

11.1.2 并发和并行

        多个线程同时处理一个任务叫并发

        多个线程处理不同任务叫并行。并行对是否同时开始没有要求。

11.2 线程 ID,pthread_self

        线程用的Posix

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

上下文:

        进程中已经执行过正在执行以及将要执行指令和数据。这些指令和数据分别被称为上文正文下文

        进程ID使用 pid_t 来表示。线程 ID变量类型 pthread_t

        pthread_t在Linux是unsigned long类型。

        线程可通过库函数 pthread_self()来获取自己的线程 ID。使用 pthread_equal()检查两个线程ID是否相等。

#include <pthread.h>

/* 获取自己的线程ID*/
pthread_t pthread_self(void);

/* 判断两个线程ID是否相等。相等返回非0值。 */
int pthread_equal(pthread_t t1,pthread_t t2);

11.3 创建线程 pthread_create

        使用库函数 pthread_create()创建一个新的线程。

#include <pthread.h>

/* 创建一个线程 */
int pthread_create(pthread_t *thread,              //线程变量
                   const pthread_attr_t *attr,     //线程属性变量
                   void *(*start_routine) (void *),//功能函数指针
                   void *arg);                     //功能函数指针参数
/*
void *代表返回类型,
(*start_routine)代表函数指针,没有*会被编译器当成函数。
(void*)代表参数。
*/

        pthread_attr_t 是 POSIX 线程(pthread)库中线程的属性变量结构体。它允许程序员在创建线程时指定各种属性,比如线程的堆栈大小、调度策略、优先级、分离状态等。

        pthread_attr_t 是一个不透明的数据类,不提供原型,只能使用功能函数进行初始化和各种参数设置。

11.4 终止线程 pthread_exit

        1、线程使用 return 返回以后便终止了。

        2、还可以调用 pthread_exit()函数终止调用它的线程。如果进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止。

        3、调用 pthread_cancel() 取消指定的线程。

#include <pthread.h>

/*终止调用它的线程*/
void pthread_exit(void *retval);//返回值。

        线程的返回值可由另一个线程调用 pthread_join() 来获取。

        主线程终止了其他线程会正常运行。所有线程终止则进程终止。

11.5 回收线程 pthread_join

        对于父子进程来讲,父进程可通过 wait()函数(或其变体 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源。

        在线程当中,同进程下的线程是平等的,互相可通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源。

#include <pthread.h>

/*等待线程的终止,会让被等待的线程优先执行*/
int pthread_join(pthread_t thread, 
                 void **retval);//用来获取线程的退出码

        进程中的任意线程均可调用 pthread_join()函数来等待另一个线程的终止。而进程的wait/waitpid只能用于父进程监视子进程的终止状态。

11.6 取消线程 pthread_cancel

        向指定的线程发送一个请求,要求其立刻终止,称为线程取消机制

11.6.1 取消一个线程

        调用 pthread_cancel()库函数向一个指定的线程发送取消请求。不等待目标线程的终止,仅仅是提出请求。

#include <pthread.h>

/* 请求取消指定的线程。成功返回0失败返回错误码*/
int pthread_cancel(pthread_t thread);

11.6.2 取消状态以及类型

        默认情况下,调用了 pthread_cancel 后默认线程会立刻退出,但是线程也可以设置自己的取消状态和取消类型。

        通过 pthread_setcancelstate()和 pthread_setcanceltype()设置线程的取消状态和取消类型。

#include <pthread.h>

/* 线程设置自己的取消状态 */
int pthread_setcancelstate(int state,     //新状态   
                           int *oldstate);//用来保存旧状态
/*
状态state参数:
    PTHREAD_CANCEL_ENABLE 允许取消
    PTHREAD_CANCEL_DISABLE 不允许取消。该状态下收到的取消请求会被挂起,直到取消状态变成允许取消。
*/

/* 线程设置自己的取消类型*/
int pthread_setcanceltype(int type,       //新类型
                          int *oldtype);  //用来保存旧类型
/*
取消类型type参数:
    PTHREAD_CANCEL_DEFERRED    //延缓取消。即到达取消点后取消。
    PTHREAD_CANCEL_ASYNCHRONOUS//随机取消
*/

        pthread_setcancelstate()函数执行的设置取消状态和获取旧状态操作是一个原子操作

        如果线程的取消状态为 PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消类型,该类型可以通过调用 pthread_setcanceltype()函数来设置,它的参数 type 指定了需要设置的类型。有到达取消点(cancellation point)随机取消两种类型。

11.6.3 取消点

        若线程的取消状态为允许取消,而取消性类型设置为 PTHREAD_CANCEL_DEFERRED (延缓取消)时,收到其它线程发送过来的取消请求时,仅当线程抵达取消点时,取消请求才起作用。

        取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求。没有到达取消点时系统会认为线程在执行关键代码,取消会导致未知异常。

        取消点函数

取消点函数

        可通过 man 7 pthreads 查看可作为取消点的函数。

        线程在调用这些函数时收到了取消请求,才允许取消。否则推迟至走到取消点时。

        假设线程执行的是一个不含取消点的循环(for,while),那么线程永远也不会响应取消请求。

        可使用 pthread_testcancel() 函数人为设置取消点

#include <pthread.h>

/* 只作为取消点,不执行功能*/
void pthread_testcancel(void);

11.7 分离线程

        当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源。而有时程序员不关心线程状态,任它自己结束、回收,可调用 pthread_detach()将指定线程进行分离。

#include <pthread.h>
/*将指定线程分离*/
int pthread_detach(pthread_t thread);

        一旦线程处于分离状态,同进程内其他线程旧不能再使用 pthread_join()来获取其终止状态,此过程不可逆。

11.8 注册线程清理处理函数

        进程使用 atexit()函数注册进程终止处理函数,调用 exit()退出时就会执行进程终止处理函数。

        线程通过函数 pthread_cleanup_push()和 pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加和移除清理函数

#include <pthread.h>

/*向线程的清理函数栈添加清理函数*/
void pthread_cleanup_push(void (*routine)(void *),//清理函数指针
                          void *arg);             //清理函数参数

/*向线程的清理函数栈移除清理函数*/
void pthread_cleanup_pop(int execute);//为0则移除清理函数栈栈顶的清理函数
                                      //为1则移除并执行

        线程有3种情况执行清理函数:

        调用 pthread_exit()函数退出、

        被 pthread_cancle() 请求取消、

        用非零参数调用 pthread_cleanup_pop()

11.9 线程属性

        调用 pthread_create()创建线程,可使用 pthread_attr_t 参数对线程的属性变量进行设置

        当定义 pthread_attr_t 对象之后 ,需使用 pthread_attr_init()函数对该对象进行初始化 ,当对象不再使用时, 需使用 pthread_attr_destroy()函数将其销毁

#include <pthread.h>

/* 对 pthread_attr_t 对象进行初始化 */
int pthread_attr_init(pthread_attr_t *attr);

/* 对 pthread_attr_t 对象进行销毁 */ 
int pthread_attr_destroy(pthread_attr_t *attr);

        pthread_attr_t 未提供函数原型,只提供了设置各种参数的接口,包括:

        线程栈位置和大小线程调度策略和优先级,以及线程的分离状态属性等。

11.9.1 线程栈属性

        每个线程都有自己的栈空间,pthread_attr_t 数据结构中定义了栈的起始地址以及栈大小

        调用函数 pthread_attr_getstack()可以获取栈的起始地址以及栈大小

        调用函数 pthread_attr_setstack()对栈起始地址和栈大小进行设置。

#include <pthread.h>

/* 设置 pthread_attr_t 变量中 栈的地址和大小*/
int pthread_attr_setstack(pthread_attr_t *attr, //线程属性变量
                          void *stackaddr,    //栈地址
                          size_t stacksize);  //栈大小

/* 获取 pthread_attr_t 变量中的 站地址和大小 */
int pthread_attr_getstack(const pthread_attr_t *attr,//线程属性变量 
                          void **stackaddr,          //栈地址
                          size_t *stacksize);        //栈大小

11.9.2 分离状态属性

        使用 pthread_detach()函数可以将线程分离,使得线程直接由守护进程Init()接管,分离的线程在退出时,守护进程Init()会自动回收它所占用的资源。

        在创建线程时修改线程属性结构体 pthread_attr_t 中的 detachstate 属性,可以让线程创建时就处于分离状态。

        调用函数 pthread_attr_setdetachstate()设置 detachstate 线程属性,

        调用pthread_attr_getdetachstate()获取 detachstate 线程属性。

#include <pthread.h>

/*设置线程属性中的 分离状态属性*/
int pthread_attr_setdetachstate(pthread_attr_t *attr,//线程属性
                                int detachstate);    //分离状态

/*获取线程属性中的 分离状态属性*/
int pthread_attr_getdetachstate(const pthread_attr_t *attr,//线程属性
                                int *detachstate);         //分离状态

/*
分离状态 detachstate取值:
    PTHREAD_CREATE_DETACHED //分离
    PTHREAD_CREATE_JOINABLE //可回收
*/

第十二章 线程同步

12.1 为什么需要线程同步?

        线程同步是为了对共享资源的访问进行保护

        保护的目的是为了解决数据一致性问题

        数据一致性问题本质在于进程中的多线程对共享资源的并发访问

        如何解决并发访问出现数据不一致的问题?

        使用线程同步技术。实现同一时间只允许一个线程访问该变量。

12.2 互斥锁

12.2.1 互斥锁初始化

        互斥锁(mutex)又叫互斥量。

        互斥锁使用 pthread_mutex_t 结构体,在使用互斥锁之前,必须首先对它进行初始化操作,可使用初始化宏 PTHREAD_MUTEX_INITALIZER或者初始化函数 pthread_mutex_init()方式进行互斥锁初始化操作。

/*初始化宏*/
# define PTHREAD_MUTEX_INITIALIZER \
 { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#include <pthread.h>

/*初始化 pthread_mutex 互斥锁*/
int pthread_mutex_init(pthread_mutex_t *mutex,          //互斥锁变量
                       const pthread_mutexattr_t *attr);//互斥锁属性指针。NULL代表默认值

12.2.2 互斥锁获取、加锁、解锁、销毁

        调用函数 pthread_mutex_lock() 可以获取互斥锁

                        pthread_mutex_trylock() 可以非阻塞式获取互斥锁

        调用函数 pthread_mutex_unlock() 可以释放互斥锁

        调用函数 pthread_mutex_destory() 可以销毁互斥锁。

#include <pthread.h>

/* 获取互斥锁 */
int pthread_mutex_lock(pthread_mutex_t *mutex);

/* 释放互斥锁 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);

/* 非阻塞式获取互斥锁 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);、

/* 销毁互斥锁 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);//没初始化的互斥锁不能销毁

12.2.3 互斥锁死锁

        如果一个线程试图对同一个互斥锁加锁两次,会导致该线程陷入死锁。自己等自己的锁,永远阻塞。

        要避免此类死锁的问题,最简单的方式就是定义互斥锁的层级关系,按照相同的顺序对该互斥锁进行锁定。

        一般我们在程序中,会对线程锁定的第一个锁使用 pthread_mutex_lock() 获取互斥锁,对之后的其他锁使用 pthread_mutex_trylock() 获取,一旦获取失败(返回EBUSY)就释放所有的锁。

12.2.4 互斥锁的属性

        调用 pthread_mutex_init()函数初始化互斥锁时可以设置互斥锁的属性,通过参数 attr 指定。

        参数 attr 指向一个 pthread_mutexattr_t 类型对象。

        当定义 pthread_mutexattr_t 对象之后,需要使用 pthread_mutexattr_init()函数对该对象进行初始化操作,当对象不再使用时,需要使用 pthread_mutexattr_destroy()将其销毁。

#include <pthread.h>

/* 初始化 pthread_mutexattr 线程互斥锁属性对象 */
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

/* 销毁 pthread_mutexattr 线程互斥锁属性对象 */
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

        互斥锁的属性比较多,譬如进程共享属性、类型属性等等。

        互斥锁的类型属性控制着互斥锁的锁定特性,一共有 4 种互斥锁类型:

        PTHREAD_MUTEX_NORMAL      //普通属性。同一线程加锁两次会死锁。
        PTHREAD_MUTEX_ERRORCHECK  //错误检查。同一线程加锁两次会报错。
        PTHREAD_MUTEX_RECURSIVE   //可递归。  同一线程加锁多次可递归,会计数。
        PTHREAD_MUTEX_DEFAULT     //默认属性。类似普通属性,同一线程加锁两次会死锁。

        可使用 pthread_mutexattr_gettype() 得到互斥锁的类型属性

        使用 pthread_mutexattr_settype() 修改/设置互斥锁类型属性

12.3 条件变量

        条件变量用于自动阻塞线程,直到某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。

12.3.1 条件变量初始化

        条件变量使用 pthread_cond_t 数据类型来表示。

        在使用条件变量之前必须对其进行初始化。不使用条件变量了需要将其销毁。

        初始化方式有初始化宏和初始化函数两种:

        使用宏 PTHREAD_COND_INITIALIZER

        使用函数 pthread_cond_init()。

        条件变量 pthread_cond_t 的初始化方式和 pthread_mutex_t 相似。

#include <pthread.h>

/* 初始化条件变量 */
int pthread_cond_init(pthread_cond_t *cond,              //条件变量
                      const pthread_condattr_t *attr);   //条件变量属性

/* 销毁条件变量 */
int pthread_cond_destroy(pthread_cond_t *cond);

12.3.2 通知和等待条件变量

        条件变量的主要操作是发送信号等待

        pthread_cond_signal()                //通知等待队列第一个线程

        pthread_cond_broadcast()         //通知等待队列全部线程

        pthread_cond_wait()                  //线程阻塞,进入等待队列等通知

#include <pthread.h>

/* 通知等队列第一个线程 */
int pthread_cond_signal(pthread_cond_t *cond);

/* 通知等待队列全部线程 */
int pthread_cond_broadcast(pthread_cond_t *cond);

/* 线程阻塞,进入等待对列等通知。要先获得互斥锁 */
int pthread_cond_wait(pthread_cond_t *cond,    //条件变量
                      pthread_mutex_t *mutex); //互斥锁

        条件变量通常是和互斥锁一起使用,因为条件变量本身属于线程共享资源,要在互斥锁的保护下避免数据一致性问题

12.3.3 条件变量的判断条件

pthread_mutex_lock(&mutex);//上锁
     while (0 >= g_avail)
         pthread_cond_wait(&cond, &mutex);//等待条件满足
pthread_mutex_unlock(&mutex);//解锁

        必须使用 while 循环,而不是 if 语句,这是一种通用的设计原则:

        当线程从 pthread_cond_wait()返回时,并不能确定进入条件等待前的判断条件的状态是否还正确,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。因为其他线程可能导致判断条件的状态改变。

12.3.4 条件变量的属性

        调用 pthread_cond_init()函数初始化条件变量时,可以设置条件变量的属性,通过参数 attr 指定。参数 attr 指向一个 pthread_condattr_t 类型对象,该对象对条件变量属性进行定义。

        pthread_condattr_t 和 pthread_mutexattr_tpthread_attr_t 一样,都有着功能函数用来初始化和设置属性变量。

        可使用 pthread_condattr_get属性名() 得到条件变量相关属性

        使用 pthread_condattr_set属性名() 修改条件变量相关属性。

        条件变量包括两个属性:进程共享属性和时钟属性。

12.4 自旋锁

        从实现方式上来说,互斥锁是基于自旋锁实现的,所以自旋锁相较于互斥锁更加底层。

        如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得自旋锁;如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到自旋锁释放。

        互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看自旋锁是否被释放

        自旋锁的“自旋”和互斥锁的"阻塞",区别在于,自旋锁的循环查看会持续消耗CPU,而互斥锁的阻塞是在等CPU通知,不消耗CPU。

        试图对同一自旋锁加锁两次必然会导致死锁,

        而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有普通错误检查可递归默认这四种类型。当设置为 PTHREAD_MUTEX_ERRORCHECK 类型时,会进行错误检查,第二次上锁直接返回错误码,不会死锁。

        自旋锁通常用于需要保护的代码段执行时间很短的情况,持有锁的线程会很快释放锁,而“自旋”的时间也会很短。

自旋锁与互斥锁的区别:

        1、互斥锁是基于自旋锁实现的。

        2、互斥锁拿不到锁会阻塞,也就是休眠。休眠状态不消耗CPU,但是休眠和唤醒的开销比较大。而自旋锁的自旋是循环检查锁有没有被释放,会持续消耗CPU

        3、自旋锁适合快进快出的场合,内核中断中常用。互斥锁适合等待时间较长的情况。

12.4.1 自旋锁初始化

        自旋锁使用 pthread_spinlock_t 数据类型表示,

        调用 pthread_spin_init()函数对其进行初始化

        调用 pthread_spin_destroy()函数将其销毁

#include <pthread.h>

/* 自旋锁spin 初始化 */
int pthread_spin_init(pthread_spinlock_t *lock, //自旋锁对象
                      int pshared);             //进程共享属性
/*
自旋锁的进程共享参数:
    PTHREAD_PROCESS_SHARED:共享自旋锁。
    PTHREAD_PROCESS_PRIVATE:私有自旋锁。
*/

/* 自旋锁spin 销毁 */
int pthread_spin_destroy(pthread_spinlock_t *lock);

        调用 pthread_spin_init() 初始化自旋锁时可以指定自旋锁的 pshare 共享属性:

    PTHREAD_PROCESS_SHARED:共享自旋锁。
    PTHREAD_PROCESS_PRIVATE:私有自旋锁。

        共享自旋锁允许在多个进程的线程之间共享。

        私有自旋锁只有本进程的线程才能使用。

12.4.2 自旋锁加锁和解锁

        调用函数 pthread_spin_lock() 可以获取自旋锁

                        pthread_mutex_trylock() 可以非自旋式获取自旋锁,返回EBUSY错误

        调用函数 pthread_spin_unlock() 可以释放自旋锁

        调用函数 pthread_spin_destory() 可以销毁自旋锁。

#include <pthread.h>

/*自旋锁加锁*/
int pthread_spin_lock(pthread_spinlock_t *lock);

/*自旋锁加锁,拿不到就返回EBUSY错误*/
int pthread_spin_trylock(pthread_spinlock_t *lock);

/*自旋锁释放锁*/
int pthread_spin_unlock(pthread_spinlock_t *lock);

        试图对同一自旋锁加锁两次必然会导致死锁。

12.5 读写锁

        互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以加锁。

        读写锁有 3 种状态:读锁状态、写锁状态、不加锁状态。

        加了写锁,再加写锁或者读锁阻塞。

        加了读锁,可以再加读锁,但加写锁时阻塞。

         读写锁非常适合共享数据读的次数远大于写的次数的情况。读锁可以保护数据不被修改。写锁保护数据没写好时不被读到。

12.5.1 读写锁初始化

        在使用读写锁之前也必须对读写锁进行初始化操作.

        读写锁使用 pthread_rwlock_t 数据类型表示。

        读写锁的初始化也有初始化宏初始化函数两种方式。

        初始化宏 PTHREAD_RWLOCK_INITIALIZER

        初始化函数 pthread_rwlock_init(),其初始化方式与互斥锁相同。

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

        必须在定义读写锁时就对其进行初始化,不使用时销毁

#include <pthread.h>

/* 读写锁初始化 */
int pthread_rwlock_init(pthread_rwlock_t *rwlock,
                        const pthread_rwlockattr_t *attr);//读写锁属性

/* 销毁读写锁 */
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

12.5.2 读写锁上锁和解锁

        上读锁,需要调用 pthread_rwlock_rdlock()函数;

        上写锁,需要调用 pthread_rwlock_wrlock()函数。

        不管是读锁还是写锁,均可以调用 pthread_rwlock_unlock()函数解锁。

#include <pthread.h>

/*上读锁*/
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

/*上读锁,不阻塞,拿不到返回EBUSY*/
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

/*上写锁*/
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

/*上写锁,不阻塞,拿不到返回EBUSY*/
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

/*解锁*/
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

12.5.3 读写锁的属性

        读写锁初始化和自旋锁、条件变量、互斥锁、线程一样,可以通过 pthread_xxxattr_t 结构体传入初始化属性,通过 pthread_xxxattr_get/set 来获取/修改属性。

        使用读写锁的属性前要用 pthread_rwlockattr_init 功能函数初始化读写锁属性。

        使用完读写锁的属性要用 pthread_rwlockattr_destory 功能函数销毁读写锁属性。

#include <pthread.h>

/*初始化读写锁属性*/
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

/*销毁读写锁属性*/
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
#include <pthread.h>

/*获取读写锁的进程共享*/
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, //读写锁属性变量
                                   int *pshared);//进程共享状态

/*设置读写锁的进程共享属性*/
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, 
                                  int pshared);

/*
读写锁的共享属性:
    PTHREAD_PROCESS_SHARED 进程共享
    PTHREAD_PROCESS_PRIVATE 进程私有
*/

        读写锁只有一个属性,那就是进程共享状态。 

    PTHREAD_PROCESS_SHARED 进程共享
    PTHREAD_PROCESS_PRIVATE 进程私有

12.6 无名信号量

        进程间通信介绍了基于SYSTEM V标准的信号量。

        POSIX标准即提供了有名信号量接口,也提供了无名信号量接口。

        无名信号量使用 sem_t 数据类型表示。

        无名信号量由于只存在于内存中,不利于跨进程通信,常被用来做同进程下的线程间通信

12.6.1 初始化 | sem_init

        无名信号量的初始化是通过 sem_init 函数来完成的。

#include <semaphore.h>

sem_t sem;//无名信号量变量

int sem_init(sem_t *sem,             //信号量变量
             int pshared,            //信号量的进程共享属性。PTHREADSHARE/PTHREADPRIVATE
             unsigned int value);    //信号量的值

//在编译命令中添加-pthread标志来链接pthread库

12.6.2 等待信号量 | sem_wait

        调用 sem_wait() 函数阻塞式等待信号量。

#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, 
                  const struct timespec *abs_timeout);//时间戳结构体,秒数、纳秒数

12.6.3 发布信号量 | sem_post

#include <semaphore.h>

/*发布信号量。执行V操作*/
int sem_post(sem_t *sem);

12.6.4 销毁 | sem_destory()

#include <semaphore.h>

/*销毁信号量*/
int sem_destroy(sem_t *sem);

12.6.5 获取信号量的值 | sem_getvalue

#include <semaphore.h>

/*获取信号量的值。成功0,失败-1*/
int sem_getvalue(sem_t *sem, 
                 int *sval);//存储获取到的信号量值

12.7 有名信号量

        有名信号量由于其有名字(/+其他字符), 多个独立的进程可以通过名字来打开同一个信号量, 从而完成同步操作。

12.7.1 创建 | sem_open

        有名信号量类似文件IO,信号量变量 sem_t

        用 sem_open函数 提供信号量名操作标志进行打开/创建。

#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

/*POSIX有名信号量的创建/打开*/
sem_t *sem_open(const char *name,//有名信号量名称
                int oflag);//O_CREATE 不存在则创建,存在则打开
                           //O_EXCL 存在则报错

/*POSIX有名信号量的创建/打开*/
sem_t *sem_open(const char *name,  //有名信号量名称
                int oflag,         //O_CREATE 不存在则创建,存在则打开
                                   //O_EXCL 存在则报错
                mode_t mode,       //创建新的信号量的权限
                unsigned int value);//信号量初始值

/*C语言不支持重载重写,一个程序只能用一种函数,编译的时候根据参数确认链接哪种函数*/

12.7.2 关闭 | sem_close

        调用 sem_close 时,信号量的进程数的引用计数减 1。

#include <semaphore.h>

/*POSIX有名信号量的关闭*/
int sem_close(sem_t *sem);//信号量标识符ID,sem_t

12.7.3  删除 | sem_unlink

        通过 sem_unlink, 负责将该有名信号量从系统中删除。

        由于系统为信号量维护了引用计数, 所以只有当打开信号量的所有进程都关闭了之后, 才会真正地删除。

#include <semaphore.h>

/*删除有名信号量*/
int sem_unlink(const char *name);//信号量名

12.7.4 POSIX 有名/无名信号量区别

        POSIX 有名信号量只有创建和关闭不同,同时有名信号量多了个信号量删除

        有名信号量创建用 sem_open(信号量名,模式,权限,信号量值),

                        关闭用 sem_close(信号量名),删除用 sem_unlink(信号量名)。

        无名信号量创建用 sem_Init(信号量ID变量,共享权限,信号量值),

                        关闭用 sem_destory(信号量ID)

12.8 内存屏障

12.8.1 内存屏障概念

        现在大多数计算机为了提高性能采取乱序执行,可能导致程序运行过程不符合我们预期。

        内存屏障就是一类同步屏障指令,编译器和CPU在对内存随机访问的操作中的同步点

        内存屏障之前的所有读写操作都执行完后才可以开始执行之后的操作。

        语义上,内存屏障之前,写操作都要写入内存;内存屏障之后的读操作都能获得同步屏障之前的写操作的结果。

        因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。12.7.2 内存屏障是什么

        硬件层的内存屏障分为两种:Load BarrierStore Barrier读屏障写屏障

        对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

        对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

内存屏障有两个作用

        1、阻止屏障两侧的指令重排序;

        2、强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

        对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

        对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

12.8.2 为什么要有内存屏障

        乱序访问分为两类,一类是编译时乱序访问,一类是运行时乱序访问

        编译时乱序访问

        编译器对代码做出优化时,可能改变实际执行指令的顺序。

int x, y, r;
void f()
{
    x = r;
    y = 1;
}

        编译器优化后得到的汇编指令顺序,会先赋值 y,再赋值 x。

        避免此行为的办法就是使用编译器屏障(又叫优化屏障)。

         Linux内核提供了函数barrier(),用于让编译器保证其之前的内存访问先于其之后的内存访问完成。

#define barrier() __asm__ __volatile__("": : :"memory")
int x, y, r;
void f()
{
    x = r;
    __asm__ __volatile__("": : :"memory")
    y = 1;
}

        运行时乱序访问

        运行时,CPU本身是会乱序执行指令的。

        乱序处理器(out-of-order processors)会先处理那些有可用输入操作对象的指令,从而避免等待,提高效率。

        在SMP架构下,每个CPU与内存之间,都配有自己的高速缓存(Cache),以减少访问内存时的冲突。Symmetric Multi-Processing,SMP,对称多处理

采用高速缓存的写操作有两种模式:

        穿透模式,每次写时,都直接将数据写回内存中,效率相对较低;

        回写模式,写的时候先写回告诉缓存,然后由高速缓存的硬件自动将复用缓冲线(Cache Line)的数据写回内存,或者由软件主动地“冲刷”有关的缓冲线。

        正是由于使用用了高速缓存的回写模式,才导致在SMP架构下,对高速缓存的运用可能改变对内存操作的顺序。

// thread 0 -- 在CPU0上运行
x = 42;
ok = 1;
 
// thread 1 – 在CPU1上运行
while(!ok);
print(x);

         由于存在高速缓存,上述代码有可能先将ok=1优先从高速缓存刷新进主存(SRAM),而此时x的主存还没有更新。

12.8.3 现有架构内存屏障操作

x86/64
x86/64系统架构提供了三种内存屏障指令: (1) sfence; (2) lfence; (3) mfence。

// linux-6.9.1/arch/arm64/include/asm/barrier.h
#define __mb()        asm volatile("mfence":::"memory")    //完全内存屏障
#define __rmb()        asm volatile("lfence":::"memory")   //读内存屏障
#define __wmb()        asm volatile("sfence" ::: "memory") //写内存屏障

sfence:可以看做是一定将数据写入内存

lfence:可以看做是一定将从内存中读出来,而不是从高速缓存读出来。

mfence则正好结合了两项操作。

intel和arm都有x86/64架构芯片

arm64

arm64系统提供的下面几种内存屏障指令:

#define isb()           asm volatile("isb" : : : "memory")
#define dmb(opt)        asm volatile("dmb " #opt : : : "memory")
#define dsb(opt)        asm volatile("dsb " #opt : : : "memory")
 
#define __smp_mb()        dmb(ish)
#define __smp_rmb()       dmb(ishld)
#define __smp_wmb()       dmb(ishst)
 
#define __mb()            dsb(sy)
#define __rmb()           dsb(ld)
#define __wmb()           dsb(st)
 
#define __dma_mb()        dmb(osh)
#define __dma_rmb()       dmb(oshld)

12.8.4 内存一致性模型

        顺序一致性模型完全存储定序模型部分存储定序模型宽松存储定序模型

        对于内存的访问,我们只关心两种类型的指令的顺序,一种是读取,一种是写入。对于读取和加载指令来说,它们两两一起,一共有四种组合:

  1. LoadLoad:前一条指令是读取,后一条指令也是读取。
  2. LoadStore:前一条指令是读取,后一条指令是写入。
  3. StoreLoad:前一条指令是写入,后一条指令是读取。
  4. StoreStore:前一条指令是写入,后一条指令也是写入。

 顺序一致性模型(不优化)

        CPU会按照代码次序来执行所有的读取与写入指令。也就是强顺序执行。不做任何优化。

完全存储定序模型(优化写读)

        允许对StoreLoad指令组合进行重排序。对于先写后读的情况,允许优化成先读后写。

部分存储定序模型(优化写读、写写)

        允许对StoreLoad、StoreStore指令组合进行重排序。

宽松存储模型(读读、读写、写读、写写都优化)

        允许 Store和Load的4种组合都被优化。

        注意,这里的“优化”指的是多线程情况下的优化。比如 i = 7;printf("%d",i);在单线程环境下是顺序执行的,但在多线程环境下执行同一段代码可能另一个线程先读,另一个再写。

  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1 目标检测的定义 目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的类别和位置,是计算机视觉领域的核心问题之一。由于各类物体有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具有挑战性的问题。 目标检测任务可分为两个关键的子任务,目标定位和目标分类。首先检测图像中目标的位置(目标定位),然后给出每个目标的具体类别(目标分类)。输出结果是一个边界框(称为Bounding-box,一般形式为(x1,y1,x2,y2),表示框的左上角坐标和右下角坐标),一个置信度分数(Confidence Score),表示边界框中是否包含检测对象的概率和各个类别的概率(首先得到类别概率,经过Softmax可得到类别标签)。 1.1 Two stage方法 目前主流的基于深度学习的目标检测算法主要分为两类:Two stage和One stage。Two stage方法将目标检测过程分为两个阶段。第一个阶段是 Region Proposal 生成阶段,主要用于生成潜在的目标候选框(Bounding-box proposals)。这个阶段通常使用卷积神经网络(CNN)从输入图像中提取特征,然后通过一些技巧(如选择性搜索)来生成候选框。第二个阶段是分类和位置精修阶段,将第一个阶段生成的候选框输入到另一个 CNN 中进行分类,并根据分类结果对候选框的位置进行微调。Two stage 方法的优点是准确度较高,缺点是速度相对较慢。 常见Tow stage目标检测算法有:R-CNN系列、SPPNet等。 1.2 One stage方法 One stage方法直接利用模型提取特征值,并利用这些特征值进行目标的分类和定位,不需要生成Region Proposal。这种方法的优点是速度快,因为省略了Region Proposal生成的过程。One stage方法的缺点是准确度相对较低,因为它没有对潜在的目标进行预先筛选。 常见的One stage目标检测算法有:YOLO系列、SSD系列和RetinaNet等。 2 常见名词解释 2.1 NMS(Non-Maximum Suppression) 目标检测模型一般会给出目标的多个预测边界框,对成百上千的预测边界框都进行调整肯定是不可行的,需要对这些结果先进行一个大体的挑选。NMS称为非极大值抑制,作用是从众多预测边界框中挑选出最具代表性的结果,这样可以加快算法效率,其主要流程如下: 设定一个置信度分数阈值,将置信度分数小于阈值的直接过滤掉 将剩下框的置信度分数从大到小排序,选中值最大的框 遍历其余的框,如果和当前框的重叠面积(IOU)大于设定的阈值(一般为0.7),就将框删除(超过设定阈值,认为两个框的里面的物体属于同一个类别) 从未处理的框中继续选一个置信度分数最大的,重复上述过程,直至所有框处理完毕 2.2 IoU(Intersection over Union) 定义了两个边界框的重叠度,当预测边界框和真实边界框差异很小时,或重叠度很大时,表示模型产生的预测边界框很准确。边界框A、B的IOU计算公式为: 2.3 mAP(mean Average Precision) mAP即均值平均精度,是评估目标检测模型效果的最重要指标,这个值介于0到1之间,且越大越好。mAP是AP(Average Precision)的平均值,那么首先需要了解AP的概念。想要了解AP的概念,还要首先了解目标检测中Precision和Recall的概念。 首先我们设置置信度阈值(Confidence Threshold)和IoU阈值(一般设置为0.5,也会衡量0.75以及0.9的mAP值): 当一个预测边界框被认为是True Positive(TP)时,需要同时满足下面三个条件: Confidence Score > Confidence Threshold 预测类别匹配真实值(Ground truth)的类别 预测边界框的IoU大于设定的IoU阈值 不满足条件2或条件3,则认为是False Positive(FP)。当对应同一个真值有多个预测结果时,只有最高置信度分数的预测结果被认为是True Positive,其余被认为是False Positive。 Precision和Recall的概念如下图所示: Precision表示TP与预测边界框数量的比值 Recall表示TP与真实边界框数量的比值 改变不同的置信度阈值,可以获得多组Precision和Recall,Recall放X轴,Precision放Y轴,可以画出一个Precision-Recall曲线,简称P-R
图像识别技术在病虫害检测中的应用是一个快速发展的领域,它结合了计算机视觉和机器学习算法来自动识别和分类植物上的病虫害。以下是这一技术的一些关键步骤和组成部分: 1. **数据收集**:首先需要收集大量的植物图像数据,这些数据包括健康植物的图像以及受不同病虫害影响的植物图像。 2. **图像预处理**:对收集到的图像进行处理,以提高后续分析的准确性。这可能包括调整亮度、对比度、去噪、裁剪、缩放等。 3. **特征提取**:从图像中提取有助于识别病虫害的特征。这些特征可能包括颜色、纹理、形状、边缘等。 4. **模型训练**:使用机器学习算法(如支持向量机、随机森林、卷积神经网络等)来训练模型。训练过程中,算法会学习如何根据提取的特征来识别不同的病虫害。 5. **模型验证和测试**:在独立的测试集上验证模型的性能,以确保其准确性和泛化能力。 6. **部署和应用**:将训练好的模型部署到实际的病虫害检测系统中,可以是移动应用、网页服务或集成到智能农业设备中。 7. **实时监测**:在实际应用中,系统可以实时接收植物图像,并快速给出病虫害的检测结果。 8. **持续学习**:随着时间的推移,系统可以不断学习新的病虫害样本,以提高其识别能力。 9. **用户界面**:为了方便用户使用,通常会有一个用户友好的界面,显示检测结果,并提供进一步的指导或建议。 这项技术的优势在于它可以快速、准确地识别出病虫害,甚至在早期阶段就能发现问题,从而及时采取措施。此外,它还可以减少对化学农药的依赖,支持可持续农业发展。随着技术的不断进步,图像识别在病虫害检测中的应用将越来越广泛。
1 目标检测的定义 目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的类别和位置,是计算机视觉领域的核心问题之一。由于各类物体有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具有挑战性的问题。 目标检测任务可分为两个关键的子任务,目标定位和目标分类。首先检测图像中目标的位置(目标定位),然后给出每个目标的具体类别(目标分类)。输出结果是一个边界框(称为Bounding-box,一般形式为(x1,y1,x2,y2),表示框的左上角坐标和右下角坐标),一个置信度分数(Confidence Score),表示边界框中是否包含检测对象的概率和各个类别的概率(首先得到类别概率,经过Softmax可得到类别标签)。 1.1 Two stage方法 目前主流的基于深度学习的目标检测算法主要分为两类:Two stage和One stage。Two stage方法将目标检测过程分为两个阶段。第一个阶段是 Region Proposal 生成阶段,主要用于生成潜在的目标候选框(Bounding-box proposals)。这个阶段通常使用卷积神经网络(CNN)从输入图像中提取特征,然后通过一些技巧(如选择性搜索)来生成候选框。第二个阶段是分类和位置精修阶段,将第一个阶段生成的候选框输入到另一个 CNN 中进行分类,并根据分类结果对候选框的位置进行微调。Two stage 方法的优点是准确度较高,缺点是速度相对较慢。 常见Tow stage目标检测算法有:R-CNN系列、SPPNet等。 1.2 One stage方法 One stage方法直接利用模型提取特征值,并利用这些特征值进行目标的分类和定位,不需要生成Region Proposal。这种方法的优点是速度快,因为省略了Region Proposal生成的过程。One stage方法的缺点是准确度相对较低,因为它没有对潜在的目标进行预先筛选。 常见的One stage目标检测算法有:YOLO系列、SSD系列和RetinaNet等。 2 常见名词解释 2.1 NMS(Non-Maximum Suppression) 目标检测模型一般会给出目标的多个预测边界框,对成百上千的预测边界框都进行调整肯定是不可行的,需要对这些结果先进行一个大体的挑选。NMS称为非极大值抑制,作用是从众多预测边界框中挑选出最具代表性的结果,这样可以加快算法效率,其主要流程如下: 设定一个置信度分数阈值,将置信度分数小于阈值的直接过滤掉 将剩下框的置信度分数从大到小排序,选中值最大的框 遍历其余的框,如果和当前框的重叠面积(IOU)大于设定的阈值(一般为0.7),就将框删除(超过设定阈值,认为两个框的里面的物体属于同一个类别) 从未处理的框中继续选一个置信度分数最大的,重复上述过程,直至所有框处理完毕 2.2 IoU(Intersection over Union) 定义了两个边界框的重叠度,当预测边界框和真实边界框差异很小时,或重叠度很大时,表示模型产生的预测边界框很准确。边界框A、B的IOU计算公式为: 2.3 mAP(mean Average Precision) mAP即均值平均精度,是评估目标检测模型效果的最重要指标,这个值介于0到1之间,且越大越好。mAP是AP(Average Precision)的平均值,那么首先需要了解AP的概念。想要了解AP的概念,还要首先了解目标检测中Precision和Recall的概念。 首先我们设置置信度阈值(Confidence Threshold)和IoU阈值(一般设置为0.5,也会衡量0.75以及0.9的mAP值): 当一个预测边界框被认为是True Positive(TP)时,需要同时满足下面三个条件: Confidence Score > Confidence Threshold 预测类别匹配真实值(Ground truth)的类别 预测边界框的IoU大于设定的IoU阈值 不满足条件2或条件3,则认为是False Positive(FP)。当对应同一个真值有多个预测结果时,只有最高置信度分数的预测结果被认为是True Positive,其余被认为是False Positive。 Precision和Recall的概念如下图所示: Precision表示TP与预测边界框数量的比值 Recall表示TP与真实边界框数量的比值 改变不同的置信度阈值,可以获得多组Precision和Recall,Recall放X轴,Precision放Y轴,可以画出一个Precision-Recall曲线,简称P-R
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值