Linux系统编程笔记

文章目录


根据博客学习linux系统编程所记下的笔记 , 来自https://tennysonsky.blog.csdn.net/

1.Linux系统编程

Linux操作内核的系统调用 有三种方式

  • shell,通过shell命令 由shell解释器操作内核的系统调用
  • 库函数,用户通过应用层库函数的接口,比如fread对内核的系统调用进行操作
  • 应用层系统调用,可以直接对内核的系统进行调用

2.文件IO

2.1 文件描述符

文件描述符是非负整数,打开现存文件或新建文件时,内核会返回一个文件描述符,文件描述符用来指定已打开的文件。

在系统调用(文件IO)中文件描述符起到对文件的标识作用,如果要操作文件,就是对文件描述符进行操作

当一个程序运行或者一个进程启动时,系统会自动创建三个文件描述符

#define STDIN_FILENO   0   //标准输入的文件描述符
#define STDOUT_FILENO  1   //标准输出的文件描述符
#define STDERR_FILENO  2   //标准错误的文件描述符

如果自己打开文件,会返回文件描述符,而文件描述符一般按照从小到大依次创建

Linux 中一个进程最多只能打开 NR_OPEN_DEFAULT (即1024)个文件(windos 8191个),故当文件不再使用时应及时调用 close() 函数关闭文件。

2.2 open

       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>
	   /*
	   pathname:路径   flags:open行为标志位  mode:权限
	   */
	   //文件存在时使用
       int open(const char *pathname, int flags);
		//文件不存在时使用
       int open(const char *pathname, int flags, mode_t mode);

       int creat(const char *pathname, mode_t mode);

       int openat(int dirfd, const char *pathname, int flags);
       int openat(int dirfd, const char *pathname, int flags, mode_t mode);

20150603143219626

20150603143255350

2.3 perror

#include <stdio.h>
#include <errno.h>
//输出函数调用失败的错误消息 会在你输入的字符串后面拼接错误信息 
void perror(const char *s);

const char * const sys_errlist[];
int sys_nerr;
//error是一个全局变量 通过他获得错误码
int errno;
cat /usr/include/asm-generic/errno-base.h  //查询错误码消息的定义
#define	EPERM		 1	/* Operation not permitted */
#define	ENOENT		 2	/* No such file or directory */
#define	ESRCH		 3	/* No such process */
#define	EINTR		 4	/* Interrupted system call */
#define	EIO		 5	/* I/O error */
#define	ENXIO		 6	/* No such device or address */
#define	E2BIG		 7	/* Argument list too long */
#define	ENOEXEC		 8	/* Exec format error */
#define	EBADF		 9	/* Bad file number */
#define	ECHILD		10	/* No child processes */
#define	EAGAIN		11	/* Try again */
#define	ENOMEM		12	/* Out of memory */
#define	EACCES		13	/* Permission denied */
#define	EFAULT		14	/* Bad address */
#define	ENOTBLK		15	/* Block device required */
#define	EBUSY		16	/* Device or resource busy */
#define	EEXIST		17	/* File exists */
#define	EXDEV		18	/* Cross-device link */
#define	ENODEV		19	/* No such device */
#define	ENOTDIR		20	/* Not a directory */
#define	EISDIR		21	/* Is a directory */
#define	EINVAL		22	/* Invalid argument */
#define	ENFILE		23	/* File table overflow */
#define	EMFILE		24	/* Too many open files */
#define	ENOTTY		25	/* Not a typewriter */
#define	ETXTBSY		26	/* Text file busy */
#define	EFBIG		27	/* File too large */
#define	ENOSPC		28	/* No space left on device */
#define	ESPIPE		29	/* Illegal seek */
#define	EROFS		30	/* Read-only file system */
#define	EMLINK		31	/* Too many links */
#define	EPIPE		32	/* Broken pipe */
#define	EDOM		33	/* Math argument out of domain of func */
#define	ERANGE		34	/* Math result not representable */

2.4 close

成功返回 0 失败返回-1

 #include <unistd.h>
//关闭文件描述符
int close(int fd);

2.5 write

 #include <unistd.h>
//fd: 文件描述符
//addr: 数据首地址
//count: 写入数据的长度(字节),一般情况下,数据有多少,就往文件里写多少,不能多也不能少
ssize_t write(int fd, const void *buf, size_t count);

返回值 成功:实际写入数据的字节个数

​ 失败:-1

2.6 read

 #include <unistd.h>
//fd: 文件描述符
//addr: 内存首地址
//count: 读取的字节个数
ssize_t read(int fd, void *buf, size_t count);

返回值 成功:实际读取数据的字节个数

​ 失败:-1

注意 读取到文件末尾返回0

2.7 remove

 #include <stdio.h>
//删除
int remove(const char *pathname);

2.8 系统调用与库函数

2.8.1 不需要系统调用

不需要切换到内核空间就可以完全全部功能 比如strcpy bzero等字符串操作函数

2.8.1 需要系统调用

需要切换到内核空间 这类函数通过封装系统调用去实现对应的功能

2.8.1 库函数与系统调用关系

并不是所有系统调用都被封装成了库函数 很多功能必须通过系统调用才能实现

系统调用是需要时间的 CPU工作正在内核态 在系统调用发生的时候需要保存用户态和栈和内存环境

然后转入内核态工作

结束后 又要切换回用户态,这种切换会消耗很多时间

库函数访问文件时根据需要 设置不同类型的缓冲区,从而直接减少了IO系统调用的次数。

大部分库函数其实也都调用了系统调用 只不过设置了缓冲区

3.进程

3.1 定义

程序:程序的存放在存储介质上的一种可执行文件

进程:进程是程序的执行实例,包括程序计数器,寄存器,和当前变量的值。程序是静态的,进程是动态的

程序是一些指令的有序集合,进程是程序执行的过程,进程的状态是变化的,其包括进程的创建、调度和消亡

在 Linux 系统中,操作系统是通过进程去完成一个一个的任务,进程是管理事务的基本单元。进程拥有自己独立的处理环境

系统资源(处理器 、存储器、I/O设备、数据、程序)

3.2 状态

就绪态:

进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间。

执行态:

该进程正在占用 CPU 运行。

等待态:

进程因不具备某些执行条件而暂时无法继续执行的状态。

就绪态和等待态都是不执行,但它们是有区别的,就绪态是指满足条件,时间没到,等待态是不满足条件。

同样的,进程的这三种状态可以相互转换

img

执行态–>等待态:

正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成等待状态

等待态–>就绪态:

处于等待态的进程,若其等待的事件发生,于是进程由等待状态变成就绪态

就绪态–>执行态:

当就绪态的进程所等待的cpu时间片一到来,进程就会从就绪态变成执行态

执行态–>就绪态:

处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出cpu,于是进程从执行状态转变成就绪状态

3.3进程控制块(PCB)

当我们运行一个程序使它成为一个进程时,系统会开辟一段内存空间存放与此进程相关的数据信息,而这个数据信息是通过结构体task_struct记录的

进程控制块是操作系统中最重要的记录型数据结构。进程控制块记录了用于描述进程进展情况及控制进程运行所需的全部信息,它是进程存在的唯一标志。

进程控制块里有很多信息,其中比较重要的是进程号,至于其他的一些信息我们不在这详细讨论。

img
img

3.3进程号

每个进程都由一个进程号来标识,其类型为 pid_t(无符号整型),进程号的范围:0~32767。进**程号总是唯一的,但进程号可以重用。**当一个进程终止后,其进程号就可以再次使用

img

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。

整个 Linux 系统的所有进程也是一个树形结构。

树根是系统自动构造的即在内核态下执行的 0 号进程,它是所有进程的祖先。进程号为 0 的进程通常是调度进程,常被称为交换进程( swapper )。由 0 号进程创建 1 号进程(内核态)

​ 1 号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后**,1 号进程调用 execve() 运行可执行程序 init,并演变成用户态 1 号进程,即 init 进程。**

​ init进程是所有进程的祖先,除0号调度进程外,所有的进程都是由init进程直接或者间接创建的

三个不同的进程号

进程号(PID):
标识进程的一个非负整型数。

父进程号(PPID):
任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。如,A 进程创建了 B 进程,A 的进程号就是 B 进程的父进程号。

进程组号(PGID):
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID) 。这个过程有点类似于 QQ 群,组相当于 QQ 群,各个进程相当于各个好友,把各个好友都拉入这个 QQ 群里,主要是方便管理,特别是通知某些事时,只要在群里吼一声,所有人都收到,简单粗暴。但是,这个进程组号和 QQ 群号是有点区别的,默认的情况下,当前的进程号会当做当前的进程组号。

3.4 geipid() getppid() getpgid()

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

       pid_t getpid(void); 			//获取本进程号(PID)
       pid_t getppid(void);			//获取调用此函数的进程的父进程号(PPID)
	   pid_t getpgid(pid_tpid);		//获取进程组号(PGID)

3.5 fork

#include <sys/types.h>
#include <unistd.h>
 
//用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
pid_t fork(void);

返回值:
成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为无符号整型。
失败:返回 -1

失败的两个主要原因是:
(1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
(2)系统内存不足,这时 errno 的值被设置为 ENOMEM。

如果不区分父子进程的代码区 那么同样的指令将会被父子进程都去运行两次

简单来说, 一个进程调用 fork() 函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (Copy- On-Write,COW技术) 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。

image-20220330142028761

可以简单认为父子进程的代码一样的。这样的话,父进程做了什么事情,子进程也做什么事情(如上面的例子),不能实现满足我们实现多任务的要求,那要想个办法区别父子进程,这就通过 fork() 的返回值。

利用返回值区分父子进程

需要注意的是,在子进程的地址空间里,子进程是从 fork() 这个函数后才开始执行代码。

一般来说,在 fork() 之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法

不要认为父进程执行完了才会进行子进程

image-20220330142908855

下面的例子,为验证父子进程各自的地址空间是独立的

通过得知,在子进程修改变量 a,b 的值,并不影响到父进程 a,b 的值。

image-20220330143938327

但是子进程会继承父进程的一些公有空间,比如磁盘空间,内核空间

文件描述符的偏移量保存在内核空间中,所以如果父进程改了偏移量,子进程获取的偏移量是改变之后的

image-20220330145908094

3.6 进程的挂起(sleep)

进程在一定的时间内没任何操作 称为进程的挂起,就是把进程转到等待态

3.7进程的等待

3.7.1 wait

父子进程之间有时需要简单的进程间同步,如父进程等待子进程结束

#include <sys/types.h>
#include <sys/wait.h>
//status: 进程退出时的状态信息。
pid_t wait(int *wstatus);

参数:

如果参数 status 的值不是 NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。

这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。

返回值:

成功:已经结束子进程的进程号

失败:-1

image-20220330213836708

下面我们来学习一下其中最常用的两个宏定义,取出子进程的退出信息:

WIFEXITED(status)

如果子进程是正常终止的,取出的字段值非零

WEXITSTATUS(status)

返回子进程的退出状态,退出状态保存在 status 变量的 8~16 位。在用此宏前应先用宏 WIFEXITED 判断子进程是否正常退出,正常退出才可以使用此宏。

从本质上讲,系统调用 waitpid() 和 wait() 的作用是完全相同的,但 waitpid() 多出了两个可由用户控制的参数 pid 和 options,从而为我们编程提供了另一种更灵活的方式。

3.7.2 waitpid

等待子进程终止 如果子进程终止了会回收子进程的资源

//pid > 0 等待进程 ID 等于 pid 的子进程。
//pid = 0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
//pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
//pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。

//options: options 提供了一些额外的选项来控制 waitpid()。
//0:同 wait(),阻塞父进程,等待子进程退出。
//WNOHANG;没有任何已经结束的子进程,则立即返回。
//WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,极少用到 不写了)
————————————————

pid_t waitpid(pid_t pid, int *wstatus, int options);

返回值

一共有 3 种情况:

正常返回的时候,waitpid() 返回收集到的已经子进程的进程号;

如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0;

如果调用中出错,则返回 -1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;

3.8 进程的终止(exit,_exit)

exit为库函数,_exit为系统调用

#include <unistd.h>
void _exit(int status);

continue: 结束本次循环
break: 跳出整个循环,或跳出 switch() 语句
return: 结束当前函数

我们可以通过 exit() 或 _exit() 来结束当前进程。

image-20220330214627821

3.9 进程的退出清理(atexit)

#include <stdlib.h>
//注册进程正常结束前调用的函数 进程退出执行注册函数
int atexit(void (*function)(void));

成功返回0

image-20220330220944283

3.10 进程的创建 vfork

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为无符号整型。

失败:返回 -1。

fork() 与 vfock() 都是创建一个进程,那它们有什么区别呢?

1)fork(): 父子进程的执行次序不确定。

​ vfork():保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。

2)fork(): 子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。

​ vfork():子进程共享父进程的地址空间(准确来说,在调用 exec(进程替换) 或 exit(退出进程) 之前与父进程数据是共享的

在exec子进程才会有自己的进程空间

先运行

image-20220330222451617

共享空间

image-20220330222718708

3.11 进程的替换

3.11.1 简介

在 Windows 平台下,我们可以通过双击运行可执行程序,让这个可执行程序成为一个进程;而在 Linux 平台,我们可以通过 ./ 运行,让一个可执行程序成为一个进程。

如果我们本来就运行着一个程序(进程),如何在这个进程内部启动一个外部程序,由内核将这个外部程序读入内存,使其执行起来成为一个进程呢?这里我们通过 exec 函数族实现

exec 函数族提供了六种在进程中启动另一个程序的方法。exec 函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。

进程调用一种 exec 函数时**,该进程完全由新程序替换,**而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID (当然还有父进程号、进程组号、当前工作目录……)并未改变。exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段(进程替换)

3.11.2 函数族

img

l(list):参数地址列表,以空指针结尾。

v(vector):存有各参数地址的指针数组的地址。

p(path):按 PATH 环境变量指定的目录搜索可执行文件。

e(environment):存有环境变量字符串地址的指针数组的地址。

 #include <unistd.h>

int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
                       char *const envp[]);

exec 函数族与一般的函数不同,exec 函数族中的函数执行成功后不会返回,而且,exec 函数族下面的代码执行不到。只有调用失败了,它们才会返回 -1,失败后从原程序的调用点接着往下执行。

3.11.3 execl

image-20220331075321608

3.11.4 execv

image-20220331075633890

3.11.5 execlp execvp

execlp() 和 execl() 的区别在于,execlp() 指定的可执行程序可以不带路径名,如果不带路径名的话,会在环境变量 PATH指定的目录里寻找这个可执行程序,而 execl() 指定的可执行程序,必须带上路径名。

image-20220331080121024

3.11.6 execle execve

execle() 和 execve() 改变的是 exec 启动的程序的环境变量(只会改变进程的环境变量,不会影响系统的环境变量),其他四个函数启动的程序则使用默认系统环境变量。

3.12 特殊进程

3.12.1 僵尸进程

进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵尸进程。

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。 但是仍然为其保留一定的信息**,这些信息主要主要指进程控制块的信息(包括进程号、退出状态、运行时间等)**。

这样就会导致一个问题,如果进程不调用wait() 或 waitpid() 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程

image-20220330215649281

image-20220330215658604

3.12.2 孤儿进程

父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程(Orphan Process)。孤儿进程最终会被 init 进程(进程号为 1 )所收养,并由 init 进程对它们完成状态收集工作。

孤儿进程是没有父进程的进程,为避免孤儿进程退出时无法释放所占用的资源而变为僵尸进程,进程号为 1 的 init 进程将会接受这些孤儿进程,这一过程也被称为“收养”

而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

image-20220330221507803

3.12.3 (精灵)守护进程

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等

如何查看守护进程

在终端敲:ps axj

  • a 表示不仅列当前用户的进程,也列出所有其他用户的进程
  • x 表示不仅列有控制终端的进程,也列出所有无控制终端的进程
  • j 表示列出与作业控制相关的信息

img

从上图可以看出守护进行的一些特点:

  • 守护进程基本上都是以超级用户启动( UID 为 0 )
  • 没有控制终端( TTY 为 ?)
  • 终端进程组 ID 为 -1 ( TPGID 表示终端进程组 ID)

一般情况下,守护进程可以通过以下方式启动:

  • 在系统启动时由启动脚本启动,这些启动脚本通常放在 /etc/rc.d 目录下;
  • 利用 inetd 超级服务器启动,如 telnet 等;
  • 由 cron 定时启动以及在终端用 nohup 启动的进程也是守护进程。

4.进程间通信(信号)

4.1 概述

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源(例如打开的文件描述符)。

**但是,**进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等

因此需要进程间通信( IPC:Inter Processes Communication )。

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

Linux 操作系统支持的主要进程间通信的通信机制:img

进程间通信的实质:系统只要创建一个进程 就会给当前进程分配4G的虚拟内存

4G的虚拟内存分为0-3G的用户空间和(3-4G)的内核空间,用户空间是进程私有的。

之前说的堆区,栈区,数据区,代码区都是用户自己的空间

内核空间是所有进程共有的,所以绝大多数进程间通信的基本方式,其实就是对内核空间的操作

4.2 信号概述

4.2.1 简介

信号是 Linux 进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件

信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:

img

Linux 可使用命令:kill -l(“l” 为字母),查看相应的信号。

列表中,编号为 1 ~ 31的信号为传统 UNIX 支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。非可靠信号一般都有确定的用途及含义, 可靠信号则可以让用户自定义使用。更多详情,请看《Linux信号列表》。img

4.2.2 信号的产生方式

1)当用户按某些终端键时,将产生信号。
终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT,终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT,终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP 等。

2)硬件异常将产生信号。
除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。

3)软件异常将产生信号。
当检测到某种软件条件已发生,并将其通知有关进程时,产生信号。

4)调用 kill() 函数将发送信号。
注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

5)运行 kill 命令将发送信号。
此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。

4.3 kill

#include <sys/types.h>
#include <signal.h>
//给指定进程发送信号。
//使用 kill() 函数发送信号,接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者是超级用户。

//pid > 0: 将信号传送给进程 ID 为pid的进程。
//pid = 0: 将信号传送给当前进程所在进程组中的所有进程。
//pid = -1: 将信号传送给系统内所有的进程。
//pid < -1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
int kill(pid_t pid, intsignum);

返回值:

成功:0 失败:-1

image-20220331083553917

4.4 pause

等待信号

#include <unistd.h>
//等待信号的到来(此函数会阻塞)。将调用进程挂起直至捕捉到信号为止,此函数通常用于判断信号是否已到。
int pause(void);

直到捕获到信号才返回 -1,且 errno 被设置成 EINTR。

image-20220331084018198

4.5 signal

进程收到信号后的处理方式

1)执行系统默认动作
对大多数信号来说,系统默认动作是用来终止该进程。

2)忽略此信号
接收到此信号后没有任何动作。

3)执行自定义信号处理函数
用用户定义的信号处理函数处理该信号。

注意:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。

产生一个信号,我们可以让其执行自定义信号处理函数。假如有函数 A, B, C,我们如何确定信号产生后只调用函数 A,而不是函数 B 或 C。这时候,我们需要一种规则规定,信号产生后就调用函数 A,就像交通规则一样,红灯走绿灯行,信号注册函数 signal() 就是做这样的事情。

#include <signal.h>

typedef void (*sighandler_t)(int)// 回调函数的声明
sighandler_t signal(int signum,sighandler_t handler);

image-20220331084625227

image-20220331085110662

4.6 常见信号

在这里插入图片描述

在这里插入图片描述

4.7 alarm

#include <unistd.h>
//second秒后 向调用进程发送一个SIGALRM信号,默认动作是终止调用alarm函数的进程
unsigned int alarm(unsigned int seconds);

返回值:如果以前没用设置定时器,或者设置的定时器已超时返回0,否则会返回定时器剩余的秒数,并重新设定定时器

image-20220331090358763

第一个alarm没执行完 执行第二个alarm的话,会把第一个没执行的时间直接清空不管

image-20220331090657603

4.8 raise

#include <signal.h>
//给调用进程本身发送一个信号
int raise(int sig);

In a single-threaded program it is equivalent to kill(getpid(), sig);

In a multithreaded program it is equivalent to pthread_kill(pthread_self(), sig);

image-20220331091401065

4.9 abort

 #include <stdlib.h>
//默认情况下向进程发送一个SIGABRT信号,默认情况下进程会退出
//即使SIGABRT信号被加入阻塞集,一旦进程调用了abort函数,进程也还是会被终止,且在中止的时候会刷新缓冲区,关文件描述符
void abort(void);

image-20220331091732468

4.10 可重入函数

https://blog.csdn.net/lianghe_work/article/details/47611961

4.11 信号集

为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集(信号的集合)。这个信号集有点类似于我们的 QQ 群,一个个的信号相当于 QQ 群里的一个个好友。

信号集是用来表示多个信号的数据类型(sigset_t)

信号集相关的操作主要有如下几个函数:

#include <signal.h> 
//初始化一个空的信号集
int sigemptyset(sigset_t *set);  
//初始化一个满的信号集
int sigfillset(sigset_t *set); 
//查询某个信号集里是否有某个信号
int sigismember(const sigset_t *set, int signum);  
//往信号集中添加信号
int sigaddset(sigset_t *set, int signum);  
//往信号集中移除信号
int sigdelset(sigset_t *set, int signum);

image-20220331092755367

4.12信号阻塞集

4.12.1概述

每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。

**所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。**若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。

4.12.2 sigprocmask

#include <signal.h>
//how: 信号阻塞集合的修改方法,有 3 种情况:
SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。
SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。
SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。
//set: 要操作的信号集地址。
若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
//oldset: 保存原先信号阻塞集地址


//检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

注意:**不能阻塞 SIGKILL 和 SIGSTOP 等信号,但是当 set 参数包含这些信号时 sigprocmask() 不返回错误,只是忽略它们。另外,阻塞 SIGFPE 这样的信号可能导致不可挽回的结果,**因为这些信号是由程序错误产生的,忽略它们只能导致程序无法执行而被终止。

image-20220331101238650

img

4.13 可靠信号的操作

Linux 提供了功能更强大的 sigaction() 函数,此函数可以用来检查和更改信号处理操作,可以支持可靠、实时信号的处理,并且支持信号传递信息。

https://blog.csdn.net/lianghe_work/article/details/46804469

5.进程间通信(管道)

5.1 无名管道

管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,所有的 UNIX 系统都支持这种通信机制。

1、半双工,数据在同一时刻只能在一个方向上流动

2、数据只能从管道的一端写入,从另一端读出

3、写入管道中的数据遵循先入先出的规则。

4、管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。

5、管道不是普通的文件,不属于某个文件系统,其只存在于内存中。

6、管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。

7、从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。

8、管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

我们可以类比现实生活中管子,管子的一端塞东西,管子的另一端取东西.

无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。

img

5.2 pipe()

#include <unistd.h>
//创建无名管道
//filedes: 为 int 型数组的首地址,其存放了管道的文件描述符 filedes[0]、filedes[1]。
int pipe(int filedes[2]);

当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O 的函数都可以用来操作管道( lseek() 除外)。

image-20220402140541966

5.3 管道的特点

每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者” 进程就要睡眠等待,具体过程如下图所示:

img

默认的情况下,从管道中读写数据,最主要的特点就是阻塞问题(这一特点应该记住),当管道里没有数据,另一个进程默认用 read() 函数从管道中读数据是阻塞的。

5.3.1 阻塞情况1

image-20220402140843304

可以看到子进程退出后,父进程读取数据没用读到,阻塞在了read中

5.3.2 fcntl

如何解决?可通过 fcntl() 函数设置文件的阻塞特性。

设置为阻塞:fcntl(fd, F_SETFL, 0);
设置为非阻塞:fcntl(fd, F_SETFL, O_NONBLOCK);

image-20220402141508086

5.3.3 阻塞情况2

调用 write() 函数向管道里写数据,当缓冲区已满时 write() 也会阻塞。

image-20220402141926284

阻塞在了64

5.3.3 SIGPIPE

通信过程中,别的进程先结束后,当前进程读端口关闭后,向管道内写数据时,write() 所在进程会(收到 SIGPIPE 信号)退出,收到 SIGPIPE 默认动作为中断当前进程

image-20220402142323336

linux终端直接弹出了,可见程序直接中断了,没用阻塞

5.4 命名管道

命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换

与无名管道的不同点在于

1、FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
2、当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
3、FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。

5.5 mkfifo

5.5.1 基本概念

#include <sys/types.h>
#include <sys/stat.h>
//pathname: 普通的路径名,也就是创建后 FIFO 的名字。
//mode: 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。
int mkfifo( const char *pathname, mode_t mode);

命名管道的默认操作

后期的操作,把这个命名管道当做普通文件一样进行操作:open()、write()、read()、close()。但是,和无名管道一样,操作命名管道肯定要考虑默认情况下其阻塞特性。

下面验证的是默认情况下的特点,即 open() 的时候没有指定非阻塞标志( O_NONBLOCK )。

open() 以只读方式打开 FIFO 时,要阻塞到某个进程为写而打开此 FIFO
open() 以只写方式打开 FIFO 时,要阻塞到某个进程为读而打开此 FIFO。

简单一句话**,只读等着只写,只写等着只读,只有两个都执行到,才会往下执行。**

5.5.2阻塞情况1

刚开始读时,没开启writer ,等待阻塞

image-20220402152343041

启用writer后,writer检测到reader,不阻塞,reader也检测到了writer,不阻塞

image-20220402152530572

image-20220402152543319

如果大家不想在 open() 的时候阻塞,我们可以以可读可写方式打开 FIFO 文件,这样 open() 函数就不会阻塞。

5.5.3阻塞情况2

假如 FIFO 里没有数据,调用 read() 函数从 FIFO 里读数据时 read() 也会阻塞。这个特点和无名管道是一样的

这里展示完成之后的,而在5s之前,reader那边是阻塞的

image-20220402153412435

image-20220402153250616

5.5.4 阻塞情况3

通信过程中若写进程先退出了,就算命名管道里没有数据,调用 read() 函数从 FIFO 里读数据时不阻塞;若写进程又重新运行,则调用 read() 函数从 FIFimgO 里读数据时又恢复阻塞。

5)通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到 SIGPIPE 信号)退出。
6)调用 write() 函数向 FIFO 里写数据,当缓冲区已满时 write() 也会阻塞。

5)和 6)这两个特点和无名管道是一样的,这里不再验证

命名管道可以以非阻塞标志(O_NONBLOCK)方式打开:

fd = open("my_fifo", O_WRONLY|O_NONBLOCK);  
fd = open("my_fifo", O_RDONLY|O_NONBLOCK);  

非阻塞标志(O_NONBLOCK)打开的命名管道有以下特点:
1、先以只读方式打开,如果没有进程已经为写而打开一个 FIFO, 只读 open() 成功,并且 open() 不阻塞。
2、先以只写方式打开,如果没有进程已经为读而打开一个 FIFO,只写 open() 将出错返回 -1。3、read()、write() 读写命名管道中读数据时不阻塞。

6.进程间通信(消息队列)

6.1概述

消息队列提供了一种在两个不相关的进程之间传递数据的简单高效的方法,其特点如下:

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

2)消息队列允许一个或多个进程向它写入或者读取消息。

3)与无名管道、命名管道一样,从消息队列中读出消息,消息队列中对应的数据都会被删除。

4)每个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的。

5)消息队列是消息的链表,存放在内存中,由内核维护。**只有内核重启或人工删除消息队列时,该消息队列才会被删除。**若不人工删除消息队列,消息队列会一直存在于系统中。

#include <sys/msg.h>  
#include <sys/types.h>  
#include <sys/ipc.h>  
  
key_t ftok(const char *pathname, int proj_id);  
int msgget(key_t key, int msgflg);  
int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);  
int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);  
int msgctl(int msqid, int cmd, struct msqid_ds *buf);  

而在消息队列操作中,键(key)值相当于地址,消息队列标示符相当于具体的某个银行,消息类型相当于保险柜号码。

同一个键(key)值可以保证是同一个消息队列

同一个消息队列标示符才能保证不同的进程可以相互通信

同一个消息类型才能保证某个进程取出是对方的信息。

键(key)值

System V 提供的进程间通信机制需要一个 key 值,通过 key 值就可在系统内获得一个唯一的消息队列标识符。key 值可以是人为指定的,也可以通过 ftok() 函数获得。

6.2 ftok

#include <sys/types.h>
#include <sys/ipc.h>
//获取键(key)
//pathname: 路径名
//proj_id: 项目ID,非 0 整数(只有低 8 位有效)
key_t ftok(const char *pathname, int proj_id);

6.3 msgget

创建消息队列

#include <sys/msg.h>
//创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,只要用相同的 key 值就能得到同一个消息队列的标识符
//key: ftok() 返回的 key 值
//msgflg: 标识函数的行为及消息队列的权限,其取值如下:
//	IPC_CREAT:创建消息队列。
//	IPC_EXCL: 检测消息队列是否存在。
int msgget(key_t key, int msgflg);

image-20220404223257000

ipcs -q 查询消息队列

ipcrm -q [key] 删除消息队列

typedef struct _msg  
{  
    long mtype;      // 消息类型  
    char mtext[100]; // 消息正文  
    //…… ……          // 消息的正文可以有多个成员  
}MSG; 

类型下面是消息正文,正文可以有多个成员(正文成员可以是任意数据类型的)

6.4 msgsnd

将新消息添加到消息队列。

include <sys/msg.h>
//msqid: 消息队列的标识符。
//msgp:  待发送消息结构体的地址。
//msgsz: 消息正文的字节数。
//msgflg:函数的控制属性,其取值如下:
//0:msgsnd() 调用阻塞直到条件满足为止。
//IPC_NOWAIT: 若消息没有立即发送则调用该函数的进程会立即返回。
int msgsnd(  int msqid, const void *msgp, size_t msgsz, int msgflg);

6.5 msgrcv

从标识符为 msqid 的消息队列中接收一个消息。一旦接收消息成功,则消息在消息队列中被删除。

#include <sys/msg.h>
//msqid:消息队列的标识符,代表要从哪个消息列中获取消息。
//msgp: 存放消息结构体的地址。
//msgsz:消息正文的字节数。
//msgtyp:消息的类型。可以有以下几种类型:
	msgtyp = 0:返回队列中的第一个消息。
	msgtyp > 0:返回队列中消息类型为 msgtyp 的消息(常用)。
	msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
//msgflg:函数的控制属性。其取值如下:
	0msgrcv() 调用阻塞直到接收消息成功为止。
	MSG_NOERROR: 若返回的消息字节数比 nbytes 字节数多,则消息就会截短到 nbytes 字节,且不通知消息发送进程。
	IPC_NOWAIT: 调用进程会立即返回。若没有收到消息则立即返回 -1。

ssize_t msgrcv( int msqid, void *msgp,  size_t msgsz, long msgtyp, int msgflg );

注意:在获取某类型消息的时候,若队列中有多条此类型的消息,则获取最先添加的消息,即先进先出原则。

成功:读取消息的长度

失败:-1

6.6 msgctl

对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列。

#include <sys/msg.h>
//msqid:消息队列的标识符。
//cmd:函数功能的控制。其取值如下:
	IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关数据结构。
	IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf 指向的结构中。相对于,把消息队列的属性备份到 buf 
	IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中的对应值。相当于,消息队列原来的属性值清空,再由 buf 来替换。
//buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性。

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

6.7 例子

写端

image-20220407220732117

读端

image-20220407220759465

7.进程间通信(共享内存)

7.1概述

共享内存是进程间通信中最简单的方式之一。

共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

img

共享内存的特点:

共享内存是进程间共享数据的一种最快的方法。 一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。

使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。

7.2 shmget

创建或打开一块共享内存区。

#include <sys/ipc.h>
#include <sys/shm.h>
key:进程间通信键值,ftok() 的返回值。

//size:该共享存储段的长度(字节)。

//shmflg:标识函数的行为及共享内存的权限,其取值如下:
	IPC_CREAT:如果不存在就创建
	IPC_EXCL:  如果已经存在则返回失败
位或权限位:共享内存位或权限位后可以设置共享内存的访问权限,格式和 open() 函数的 mode_t 一样(open() 的使用请点此链接),但可执行权限未使用。
int shmget(key_t key, size_t size,int shmflg);

image-20220407221922533

7.3 shmat

将一个共享内存段映射到调用进程的数据段中。简单来理解,让进程和共享内存建立一种联系,让进程某个指针指向此共享内存。

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

//shmid:共享内存标识符,shmget() 的返回值。
//shmaddr:共享内存映射地址(若为 NULL 则由系统自动指定),推荐使用 NULL。
//shmflg:共享内存段的访问权限和映射条件( 通常为 0 ),具体取值如下:
	0:共享内存具有可读可写权限。
	SHM_RDONLY:只读。
	SHM_RND:(shmaddr 非空时才有效)
void *shmat(int shmid, const void *shmaddr, int shmflg);

返回值:

成功:共享内存段映射地址( 相当于这个指针就指向此共享内存 )
失败:-1

7.4 shmdt

将共享内存和当前进程分离( 仅仅是断开联系并不删除共享内存,相当于让之前的指向此共享内存的指针,不再指向)。

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

//shmaddr:共享内存映射地址
int shmdt(const void *shmaddr);

7.5 shmctl

共享内存属性的控制。

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

//shmid:共享内存标识符。
//cmd:函数功能的控制,其取值如下:
	IPC_RMID:删除。(常用 )
	IPC_SET:设置 shmid_ds 参数,相当于把共享内存原来的属性值替换为 buf 里的属性值。
	IPC_STAT:保存 shmid_ds 参数,把共享内存原来的属性值备份到 buf 里。
	SHM_LOCK:锁定共享内存段( 超级用户 )。
	SHM_UNLOCK:解锁共享内存段。
	SHM_LOCK 用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其它进程访问。其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。这样做的优势在于让共享内存一直处于内存中,从而提高程序性能。

//buf:shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

7.6 例子

写端

image-20220407223502671

读端

image-20220407223646156

8.多线程实现多任务

8.1进程和线程

进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。

为了让进程完成一定的工作,进程必须至少包含一个线程。

img

进程:直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是资源分配的最小单位。

线程存在与进程当中,是操作系统调度执行的最小单位。。

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源

线程有自己的栈,这个栈仍然是使用进程的地址空间,只是这块空间被线程标记为了栈。每个线程都会有自己私有的栈,这个栈是不可以被其他线程所访问的。

进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler,等;

线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集,等;

线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

  • 进程中的所有的线程共享相同的地址空间。
  • 任何声明为 static/extern 的变量或者堆变量可以被进程内所有的线程读写。
  • 一个线程真正拥有的唯一私有储存是处理器寄存器。
  • 线程栈可以通过暴露栈地址的方式与其它线程进行共享。

8.2 pthread

获取线程号

#include <pthread.h>
pthread_t pthread_self(void);

8.3 pthread_equal

线程号的比较

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

返回值:

相等: 非 0

不相等:0

image-20220411084956848

8.4 pthread_create

创建一个线程

#include <pthread.h>
thread:线程标识符地址。
attr:线程属性结构体地址,通常设置为 NULL。
start_routine:线程函数的入口地址。
arg:传给线程函数的参数。
int pthread_create( pthread_t *thread,
					const pthread_attr_t *attr,
					void *(*start_routine)(void *),
					void *arg );

返回值:

成功:0 失败:非 0

pthread_create() 创建的线程从指定的回调函数开始运行,该函数运行完后,该线程也就退出了。线程依赖进程存在的,共享进程的资源,如果创建线程的进程结束了,线程也就结束了。

不传参(资源共享)

image-20220411090528824

传参

image-20220411090414072

8.5 pthread_join

回收线程资源

等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。

#include <pthread.h>
thread:被等待的线程号。
retval:用来存储线程退出状态的指针的地址。
int pthread_join(pthread_t thread, void **retval);

image-20220411091103529

创建一个线程后应回收其资源,但使用 pthread_join() 函数会使调用者阻塞,Linux 还提供了非阻塞函数 pthread_detach() 来回收线程的资源。

8.6 pthread_detach

使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。

#include <pthread.h>
thread:线程号。
int pthread_detach(pthread_t thread);

返回值:

成功:0 失败:非 0

注意,调用 pthread_detach() 后再调用 pthread_join() , pthread_join() 会立马返回,调用失败。

image-20220411091646387

8.7 pthread_exit

退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。

#include <pthread.h>
retval:存储线程退出状态的指针。
void pthread_exit(void *retval);

image-20220411092027981

8.8 线程私有

在多线程程序中,经常要用全局变量来实现多个函数间的数据共享。由于数据空间是共享的,因此全局变量也为所有线程共有。

image-20220411092619554

可以看出,其中一个线程对全局变量的修改将影响到另一个线程的访问。

但有时应用程序设计中必要提供线程私有的全局变量,这个变量仅在线程中有效,但却可以跨过多个函数访问。比如在程序里可能需要每个线程维护一个链表,而会使用相同的函数来操作这个链表,最简单的方法就是使用同名而不同变量地址的线程相关数据结构。这样的数据结构可以由 Posix 线程库维护,成为线程私有数据 (Thread-specific Data,或称为 TSD)。

8.8.1 pthread_key_create

创建一个类型为 pthread_key_t 类型的私有数据变量( key )。

#include <pthread.h> 
key:在分配( malloc )线程私有数据之前,需要创建和线程私有数据相关联的键( key ),这个键的功能是获得对线程私有数据的访问权。
destructor 清理函数地址(如:fun )。当线程退出时,如果线程私有数据地址不是非 NULL,此函数会自动被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。
回调函数其定义如下:
void fun(void *arg)
{
// arg 为 key 值
}
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

8.8.2 pthread_key_delete

注销线程私有数据

int pthread_key_delete(pthread_key_t key);

8.8.3 pthread_setspecific

设置线程私有数据( key ) 和 value 关联,注意,是 value 的值(不是所指的内容)和 key 相关联。

key:线程私有数据。
value:和 key 相关联的指针。
int pthread_setspecific(pthread_key_t key, const void *value);

8.8.4 pthread_getspecific

读取线程私有数据所关联的值

void *pthread_getspecific(pthread_key_t key);

返回值:

成功:线程私有数据( key )所关联的值 失败:NULL

// this is the test code for pthread_key 
#include <stdio.h> 
#include <pthread.h> 
#include <unistd.h>

pthread_key_t key;	// 私有数据,全局变量

//清理函数
void echomsg(void* t)
{
	printf("[destructor] thread_id = %lu, param = %p\n", pthread_self(), t);
}

void* child1(void* arg)
{
	int i = 10;

	pthread_t tid = pthread_self(); //线程号
	printf("\nset key value %d in thread %lu\n", i, tid);

	pthread_setspecific(key, &i); // 设置私有数据

	printf("thread one sleep 2 until thread two finish\n\n");
	sleep(2);
	printf("\nthread %lu returns %d, add is %p\n",
		tid, *((int*)pthread_getspecific(key)), pthread_getspecific(key));
}

void* child2(void* arg)
{
	int temp = 20;

	pthread_t tid = pthread_self();  //线程号
	printf("\nset key value %d in thread %lu\n", temp, tid);

	pthread_setspecific(key, &temp); //设置私有数据

	sleep(1);
	printf("thread %lu returns %d, add is %p\n",
		tid, *((int*)pthread_getspecific(key)), pthread_getspecific(key));
}

int main(void)
{
	pthread_t tid1, tid2;
	pthread_key_create(&key, echomsg); // 创建

	pthread_create(&tid1, NULL, child1, NULL);
	pthread_create(&tid2, NULL, child2, NULL);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);

	pthread_key_delete(key); // 注销

	return 0;
}

image-20220411094028659

从运行结果来看,各线程对自己的私有数据操作互不影响。也就是说,虽然 key 是同名且全局,但访问的内存空间并不是同一个。

8.9 线程池

8.9.1 线程池基本原理

在传统服务器结构中,常是有一个总的监听线程监听有没有新的用户连接服务器,每当有一个新的用户进入,服务器就开启一个新的线程用户处理这 个用户的数据包。这个线程只服务于这个用户,当用户与服务器端关闭连接以后,服务器端销毁这个线程。

然而频繁地开辟与销毁线程极大地占用了系统的资源,而且在大量用户的情况下,系统为了开辟和销毁线程将浪费大量的时间和资源。线程池提供了一个解决外部大量用户与服务器有限资源的矛盾。

线程池和传统的一个用户对应一个线程的处理方法不同,它的基本思想就是在程序开始时就在内存中开辟一些线程,线程的数目是固定的,他们独自形成一个类,屏蔽了对外的操作,而服务器只需要将数据包交给线程池就可以了。

当有新的客户请求到达时,不是新创建一个线程为其服务,而是从“池子”中选择一个空闲的线程为新的客户请求服务,服务完毕后,线程进入空闲线程池中。

如果没有线程空闲的话,就将数据包暂时积累, 等待线程池内有线程空闲以后再进行处理。通过对多个任务重用已经存在的线程对象,降低了对线程对象创建和销毁的开销。当客户请求 时,线程对象已经存在,可以提高请求的响应时间,从而整体地提高了系统服务的表现。

8.9.2 线程池应用实例

一般来说实现一个线程池主要包括以下几个组成部分:

1)线程管理器:用于创建并管理线程池。

2)工作线程:线程池中实际执行任务的线程。在初始化线程时会预先创建好固定数目的线程在池中,这些初始化的线程一般处于空闲状态,一般不占用 CPU,占用较小的内存空间。

3)任务接口:每个任务必须实现的接口,当线程池的任务队列中有可执行任务时,被空闲的工作线程调去执行(线程的闲与忙是通过互斥量实现的),把任务抽象出来形成接口,可以做到线程池与具体的任务无关。

4)任务队列:用来存放没有处理的任务,提供一种缓冲机制,实现这种结构有好几种方法,常用的是队列,主要运用先进先出原理,另外一种是链表之类的数据结构,可以动态的为它分配内存空间,应用中比较灵活,此教程就是用到的链表。

**什么时候需要创建线程池呢?**简单的说,如果一个应用需要频繁的创建和销毁线程,而任务执行的时间又非常短,这样线程创建和销毁的带来的开销就不容忽视,这时也是线程池该出场的机会了。如果线程创建和销毁时间相比任务执行时间可以忽略不计,则没有必要使用线程池了。

8.9.3线程池示例代码

#ifndef __THREAD_POOL_H__
#define __THREAD_POOL_H__
 
#include <pthread.h>
 
 /*********************************************************************
* 任务回调函数,也可根据需要自行修改
*********************************************************************/
typedef void *(*pool_task_f)(void *arg);
 
/*********************************************************************
* 任务句柄
*********************************************************************/
typedef struct _task{
	pool_task_f process;/*回调函数,任务运行时会调用此函数,注意也可声明成其它形式*/
	void *arg;     /*回调函数的参数*/
	struct _task *next;
}pool_task;
 
/*********************************************************************
* 线程池句柄
*********************************************************************/
typedef struct
{
	pthread_t *threadid;		/* 线程号 */
	int threads_limit;			/* 线程池中允许的活动线程数目 */
	int destroy_flag;			/* 是否销毁线程池 , 0销毁,1不销毁*/
	pool_task *queue_head;	    /* 链表结构,线程池中所有等待任务 */
	int task_in_queue;			/* 当前等待队列的任务数目 */
	pthread_mutex_t queue_lock;	/* 锁 */
	pthread_cond_t queue_ready;	/* 条件变量 */
}pool_t;
 
/*********************************************************************
*功能:		初始化线程池结构体并创建线程
*参数:		
			pool:线程池句柄
			threads_limit:线程池中线程的数量
*返回值:	无
*********************************************************************/
void pool_init(pool_t *pool, int threads_limit);
 
/*********************************************************************
*功能:		销毁线程池,等待队列中的任务不会再被执行,
			但是正在运行的线程会一直,把任务运行完后再退出
*参数:		线程池句柄
*返回值:	成功:0,失败非0
*********************************************************************/
int pool_uninit(pool_t *pool);
 
/*********************************************************************
*功能:		向线程池中添加一个任务
*参数:		
			pool:线程池句柄
			process:任务处理函数
			arg:任务参数
*返回值:	0
*********************************************************************/
int pool_add_task(pool_t *pool, pool_task_f process, void *arg);
#endif
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <assert.h>
 
#include "thread_pool.h"
 
static void *pool_thread_server(void *arg);
 
/*********************************************************************
*功能:		初始化线程池结构体并创建线程
*参数:		
			pool:线程池句柄
			threads_limit:线程池中线程的数量
*返回值:	无
*********************************************************************/
void pool_init(pool_t *pool, int threads_limit)
{
	pool->threads_limit = threads_limit;
	pool->queue_head = NULL;
	pool->task_in_queue = 0;
	pool->destroy_flag = 0;
	/*创建存放线程ID的空间*/
	pool->threadid = (pthread_t *)calloc(threads_limit, sizeof(pthread_t));
	int i = 0;
	/*初始化互斥锁和条件变量*/
	pthread_mutex_init(&(pool->queue_lock), NULL);
	pthread_cond_init(&(pool->queue_ready), NULL);
	/*循环创建threads_limit个线程*/
	for (i = 0; i < threads_limit; i++){
		pthread_create(&(pool->threadid[i]), NULL, pool_thread_server, pool);
	}
	return;
}
 
/*********************************************************************
*功能:		销毁线程池,等待队列中的任务不会再被执行,
			但是正在运行的线程会一直,把任务运行完后再退出
*参数:		线程池句柄
*返回值:	成功:0,失败非0
*********************************************************************/
int pool_uninit(pool_t *pool)
{
	pool_task *head = NULL;
	int i;
	
	pthread_mutex_lock(&(pool->queue_lock));
	if(pool->destroy_flag)/* 防止两次调用 */
		return -1;
	pool->destroy_flag = 1;
	pthread_mutex_unlock(&(pool->queue_lock));
	/* 唤醒所有等待线程,线程池要销毁了 */
	pthread_cond_broadcast(&(pool->queue_ready));
	/* 阻塞等待线程退出,否则就成僵尸了 */
	for (i = 0; i < pool->threads_limit; i++)
		pthread_join(pool->threadid[i], NULL);
	free(pool->threadid);
	/* 销毁等待队列 */
	pthread_mutex_lock(&(pool->queue_lock));
	while(pool->queue_head != NULL){
		head = pool->queue_head;
		pool->queue_head = pool->queue_head->next;
		free(head);
	}
	pthread_mutex_unlock(&(pool->queue_lock));
	/*条件变量和互斥量也别忘了销毁*/
	pthread_mutex_destroy(&(pool->queue_lock));
	pthread_cond_destroy(&(pool->queue_ready));
	return 0;
}
 
/*********************************************************************
*功能:		向任务队列中添加一个任务
*参数:		
			pool:线程池句柄
			process:任务处理函数
			arg:任务参数
*返回值:	无
*********************************************************************/
static void enqueue_task(pool_t *pool, pool_task_f process, void *arg)
{
	pool_task *task = NULL;
	pool_task *member = NULL;
	
	pthread_mutex_lock(&(pool->queue_lock));
	
	if(pool->task_in_queue >= pool->threads_limit){
		printf("task_in_queue > threads_limit!\n");
		pthread_mutex_unlock (&(pool->queue_lock));
		return;
	}
	
	task = (pool_task *)calloc(1, sizeof(pool_task));
	assert(task != NULL);
	task->process = process;
	task->arg = arg;
	task->next = NULL;
	pool->task_in_queue++;
	member = pool->queue_head;
	if(member != NULL){
		while(member->next != NULL)	/* 将任务加入到任务链连的最后位置. */
			member = member->next;
		member->next = task;
	}else{
		pool->queue_head = task;	/* 如果是第一个任务的话,就指向头 */
	}
	printf("\ttasks %d\n", pool->task_in_queue);
	/* 等待队列中有任务了,唤醒一个等待线程 */
	pthread_cond_signal (&(pool->queue_ready));
	pthread_mutex_unlock (&(pool->queue_lock));
}
 
/*********************************************************************
*功能:		从任务队列中取出一个任务
*参数:		线程池句柄
*返回值:	任务句柄
*********************************************************************/
static pool_task *dequeue_task(pool_t *pool)
{
	pool_task *task = NULL;
	
	pthread_mutex_lock(&(pool->queue_lock));
	/* 判断线程池是否要销毁了 */
	if(pool->destroy_flag){
		pthread_mutex_unlock(&(pool->queue_lock));
		printf("thread 0x%lx will be destroyed\n", pthread_self());
		pthread_exit(NULL);
	}
	/* 如果等待队列为0并且不销毁线程池,则处于阻塞状态 */
	if(pool->task_in_queue == 0){
		while((pool->task_in_queue == 0) && (!pool->destroy_flag)){
			printf("thread 0x%lx is waitting\n", pthread_self());
			/* 注意:pthread_cond_wait是一个原子操作,等待前会解锁,唤醒后会加锁 */
			pthread_cond_wait(&(pool->queue_ready), &(pool->queue_lock));
		}
	}else{
		/* 等待队列长度减去1,并取出队列中的第一个元素 */
		pool->task_in_queue--;
		task = pool->queue_head;
		pool->queue_head = task->next;
		printf("thread 0x%lx received a task\n", pthread_self());
	}
	pthread_mutex_unlock(&(pool->queue_lock));
	return task;
}
 
/*********************************************************************
*功能:		向线程池中添加一个任务
*参数:		
			pool:线程池句柄
			process:任务处理函数
			arg:任务参数
*返回值:	0
*********************************************************************/
int pool_add_task(pool_t *pool, pool_task_f process, void *arg)
{
	enqueue_task(pool, process, arg);
	return 0;
}
 
/*********************************************************************
*功能:		线程池服务程序
*参数:		略
*返回值:	略
*********************************************************************/
static void *pool_thread_server(void *arg)
{
	pool_t *pool = NULL;
	
	pool = (pool_t *)arg;
	while(1){
		pool_task *task = NULL;
		task = dequeue_task(pool);
		/*调用回调函数,执行任务*/
		if(task != NULL){
			printf ("thread 0x%lx is busy\n", pthread_self());
			task->process(task->arg);
			free(task);
			task = NULL;
		}
	}
	/*这一句应该是不可达的*/
	pthread_exit(NULL);	 
	return NULL;
}

8.9.4 测试代码

#include <stdio.h>
#include <unistd.h>
 
#include "thread_pool.h"
 
void *task_test(void *arg)
{
	printf("\t\tworking on task %d\n", (int)arg);
	sleep(1);			/*休息一秒,延长任务的执行时间*/
	return NULL;
}
 
void thread_pool_demo(void)
{
	pool_t pool;
	int i = 0;
 
	pool_init(&pool, 2);//初始化一个线程池,其中创建2个线程
	sleep(1);
	for(i = 0; i < 5; i++){
		sleep(1);
		pool_add_task(&pool, task_test, (void *)i);//添加一个任务
	}
	sleep(4);
 
	pool_uninit(&pool);//删除线程池
}
 
int main (int argc, char *argv[])
{  
	thread_pool_demo();
	return 0;
}

img

9.多任务的同步和互斥

9.1 基本介绍

现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源

  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务,这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。

互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

**同步:**是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:**两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。**比如 A 任务的运行依赖于 B 任务产生的数据。

显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要安照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。

9.2 互斥锁

9.2.1 引言

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。

这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

image-20220411132443249

线程里有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

互斥锁的操作流程如下:

1)在访问共享资源后临界区域前,对互斥锁进行加锁。

2)在访问完成后释放互斥锁导上的锁。

3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

9.2.2 pthread_mutex_init

初始化一个互斥锁。

mutex:互斥锁地址。类型是 pthread_mutex_t 。
attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。
    
可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如:
pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用  pthread_mutex_init () 来完成动态初始化,不同之处在于  PTHREAD_MUTEX_INITIALIZER  宏不进行错误检查。
    
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

返回值:

成功:0,成功申请的锁默认是打开的。 失败:非 0 错误码

9.2.3 pthread_mutex_lock | pthread_mutex_trylock

对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,直到互斥锁解锁后再上锁。

int pthread_mutex_lock(pthread_mutex_t *mutex);

调用该函数时,若互斥锁未加锁,则上锁,返回 0;若互斥锁已加锁,则函数直接返回失败,即 EBUSY。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

9.2.4 pthread_mutex_unlock

int pthread_mutex_unlock(pthread_mutex_t * mutex);

9.2.5 pthread_mutex_destory

int pthread_mutex_destroy(pthread_mutex_t *mutex);

9.2.6 例子

image-20220411133350772

9.3 读写锁

9.3.1 引言

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点如下:

1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。

2)如果有其它线程写数据,则其它线程都不允许读、写操作。

读写锁分为读锁和写锁,规则如下:

1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。

2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

img

传参和互斥锁基本一致

9.4 信号量

9.4.1 概述

信号量概述
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。

信号量主要用于进程或线程间的同步和互斥这两种典型情况。

用于互斥

img

用于同步

img

在 POSIX 标准中,信号量分两种,一种是无名信号量,一种是有名信号量。

无名信号量一般用于线程间同步或互斥

而有名信号量一般用于进程间同步或互斥。它们的区别和管道及命名管道的区别类似,无名信号量则直接保存在内存中,而有名信号量要求创建一个文件。

9.4.2 无名信号量

无名信号量基本操作

sem_init

创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化

#include <semaphore.h>
sem:信号量的地址。
pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
value:信号量的初始值。
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem_wait | sem_trywait

信号量 P 操作(减 1)

将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。

#include <semaphore.h>
sem:信号量的地址。
int sem_wait(sem_t *sem);

以非阻塞的方式来对信号量进行减 1 操作。若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。

int sem_trywait(sem_t *sem);
sem_post

信号量 V 操作(加 1)

将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。

#include <semaphore.h>
sem:信号量的地址。
int sem_post(sem_t *sem);
sem_getvalue

获取 sem 标识的信号量的值,保存在 sval 中。

#include <semaphore.h>
sem:信号量地址。
sval:保存信号量值的地址。
int sem_getvalue(sem_t *sem, int *sval);
sem_destroy
#include <semaphore.h>
int sem_destroy(sem_t *sem);
例子

image-20220411141022148

9.4.3 有名信号量

create
当有名信号量存在时使用:
sem_t *sem_open(const char *name, int oflag);


当有名信号量不存在时使用:
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

name:信号量文件名。注意,不能指定路径名。因为有名信号量,默认放在/dev/shm

img

flags:sem_open() 函数的行为标志。

mode:文件权限(可读、可写、可执行)的设置。

value:信号量初始值。

sem_close

关闭有名信号量。

int sem_close(sem_t *sem);
sem_unlink

删除有名信号量的文件。

int sem_unlink(const char *name);
例子
void printer(sem_t* sem, char* str)
{
    sem_wait(sem);  //信号量减一  
    while (*str != '\0')
    {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");

    sem_post(sem);  //信号量加一  
}

int main(int argc, char* argv[])
{
    pid_t pid;
    sem_t* sem = NULL;

    pid = fork(); //创建进程  
    if (pid < 0) { //出错  
        perror("fork error");

    }
    else if (pid == 0) { //子进程  

       //跟open()打开方式很相似,不同进程只要名字一样,那么打开的就是同一个有名信号量  
        sem = sem_open("name_sem1", O_CREAT | O_RDWR, 0666, 1); //信号量值为 1  
        if (sem == SEM_FAILED) { //有名信号量创建失败  
            perror("sem_open");
            return -1;
        }

        char* str1 = "hello";
        printer(sem, str1); //打印  

        sem_close(sem); //关闭有名信号量  

        _exit(1);
    }
    else if (pid > 0) { //父进程  

       //跟open()打开方式很相似,不同进程只要名字一样,那么打开的就是同一个有名信号量  
        sem = sem_open("name_sem1", O_CREAT | O_RDWR, 0666, 1); //信号量值为 1  
        if (sem == SEM_FAILED) {//有名信号量创建失败  
            perror("sem_open");
            return -1;
        }

        char* str2 = "world";
        printer(sem, str2); //打印  

        sem_close(sem); //关闭有名信号量  

        waitpid(pid, NULL, 0); //等待子进程结束  
    }

    sem_unlink("name_sem1");//删除有名信号量  

    return 0;
}

image-20220411194222030

#include <semaphore.h>
sem:信号量地址。
sval:保存信号量值的地址。
int sem_getvalue(sem_t *sem, int *sval);


#### sem_destroy

```c
#include <semaphore.h>
int sem_destroy(sem_t *sem);
例子

[外链图片转存中…(img-5z4gqOQs-1649984831176)]

9.4.3 有名信号量

create
当有名信号量存在时使用:
sem_t *sem_open(const char *name, int oflag);


当有名信号量不存在时使用:
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

name:信号量文件名。注意,不能指定路径名。因为有名信号量,默认放在/dev/shm

[外链图片转存中…(img-KoyI6jaw-1649984831177)]

flags:sem_open() 函数的行为标志。

mode:文件权限(可读、可写、可执行)的设置。

value:信号量初始值。

sem_close

关闭有名信号量。

int sem_close(sem_t *sem);
sem_unlink

删除有名信号量的文件。

int sem_unlink(const char *name);
例子
void printer(sem_t* sem, char* str)
{
    sem_wait(sem);  //信号量减一  
    while (*str != '\0')
    {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");

    sem_post(sem);  //信号量加一  
}

int main(int argc, char* argv[])
{
    pid_t pid;
    sem_t* sem = NULL;

    pid = fork(); //创建进程  
    if (pid < 0) { //出错  
        perror("fork error");

    }
    else if (pid == 0) { //子进程  

       //跟open()打开方式很相似,不同进程只要名字一样,那么打开的就是同一个有名信号量  
        sem = sem_open("name_sem1", O_CREAT | O_RDWR, 0666, 1); //信号量值为 1  
        if (sem == SEM_FAILED) { //有名信号量创建失败  
            perror("sem_open");
            return -1;
        }

        char* str1 = "hello";
        printer(sem, str1); //打印  

        sem_close(sem); //关闭有名信号量  

        _exit(1);
    }
    else if (pid > 0) { //父进程  

       //跟open()打开方式很相似,不同进程只要名字一样,那么打开的就是同一个有名信号量  
        sem = sem_open("name_sem1", O_CREAT | O_RDWR, 0666, 1); //信号量值为 1  
        if (sem == SEM_FAILED) {//有名信号量创建失败  
            perror("sem_open");
            return -1;
        }

        char* str2 = "world";
        printer(sem, str2); //打印  

        sem_close(sem); //关闭有名信号量  

        waitpid(pid, NULL, 0); //等待子进程结束  
    }

    sem_unlink("name_sem1");//删除有名信号量  

    return 0;
}

[外链图片转存中…(img-2HDaQ4Sc-1649984831178)]

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

这里煤球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值