进程 | 线程 | 进程间通信 |
---|---|---|
进程介绍 | 线程介绍 | 管道 FIFO有名管道 |
守护进程 | 线程同步 | XSI-IPC :消息队列 信号量 共享存储 |
线程控制 | POSIX信号量 套接字 |
在线markdown版本:Unix环境高级编程note
C语言
- void * malloc(size_t n); 未初始化的,memset初始化
- void free(void * p); 只是释放指针指向的内容 野指针,null
- void *calloc(size_t n, size_t size); 初始化为0,适合为数组申请空间
- void * realloc(void * p, size_t n); 将指针 p指向的内存块的大小改变为n字节,可能新地址
高级I/O
进程
进程介绍
fork函数创建子进程,通常情况用来执行另一个程序的,如果是执行同一个进程那么多数采用线程。
vfork 和fork 之间的另一个区别是:vfork 保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
当子进程退出时,系统不会立即删除该子进程的进程描述符,并将进程状态设置为Z(zombie),然后等待父进程处理子进程的退出。如果父进程不处理,那么子进程就会一直处于僵尸状态。
父进程处理子进程使用wait函数族。当使用wait函数处理完子进程的终止状态后,子进程才彻底的消失了,在次调用wait函数处理该子进程将会出错。
在使用wait函数时,一般等待三种进程的退出
- 等待任意一子进程退出
- 等待具体的某个子进程退出
- 等待某个进程组的任意一子进程退出
一般情况下wait函数的目的就是取出子进程的终止状态,但是通过设置一些选项,我们可以控制wait的一些行为,比如非阻塞式的,只查看是否有子进程退出,而不取出该子进程的终止状态等。
wait阻塞、waitpid不阻塞
popen、system函数(调用了fork、exec、waitpid,三种返回值)
system()来执行一个shell命令,popen()也是执行shell命令并且通过管道和shell命令进行通信。
system在执行期间调用进程会一直等待shell命令执行完成(waitpid等待子进程结束)才返回,但是popen无须等待shell命令执行完成就返回了。我们可以理解system为串行执行,在执行期间调用进程放弃了”控制权”,popen为并行执行。如果你没有在调用popen后调用pclose那么这个子进程就可能变成”僵尸”。
守护进程
- 一般在系统启动时装入,仅在系统关闭时终止。大多数守护进程以超级用户特权运行。
- 用户层守护进程的父进程是 init 进程。
- 所有的守护进程都没有控制终端,其终端名设置为问号。
- 内核守护进程以无控制终端方式启动。
- 用户层守护进程可以通过调用 setsid 实现。
守护进程是没有控制终端的,无法将自己的消息输出到标准输出或标准错误上,需要一个集中的守护进程记录设施,即 syslog。
主要有 3 中产生日志消息的方式:
1.内核例程调用 log 函数
2.大多数用户进程调用 syslog 函数
3.将日志消息发送到 UDP 的 514 端口
syslogd 守护进程接收这些日志消息,在其启动前会读取配置文件(/etc/syslog.conf),以决定各类消息的处理方式。
1 |
|
注:在没有调用 openlog 的情况下,先调用了 syslog,会自动调用 openlog。
ident 参数指向的字符串会被加到日志消息中去,因此一般指定为程序名称。
option 参数指定各种选项的位屏蔽
facility 参数可选值见图
priority 参数包含 facility 和 level 的组合,如果参数中没有指定 facility,则会使用 openlog 中指定的 facility,如果没有调用 openlog,那么会使用默认值 LOG_USER。
守护进程编程规则
1.调用 umask 函数将文件模式创建屏蔽字设置为指定值(通常为 0)。守护进程可能需要创建一些文件,如果使用继承的屏蔽字,可能文件的权限会不符合预期。
2.调用 fork 后,使父进程 exit。这样可以保证子进程不是进程组的组长进程。
3.调用 setsid 创建新会话。这可以保证当前进程没有控制终端,且成为新会话的首进程和新进程组的组长进程。
4.将当前工作目录改为根目录或某个指定位置。
5.关闭不再需要的文件描述符。可以使用 getrlimit 函数获取最高文件描述符值,并关闭直到该值的所有描述符。
6.某些守护进程将文件描述符 0、1 和 2 指向 /dev/null,这样任何需要输入输出的库例程都不会产生影响。
单实例守护进程
某些守护进程在同一时刻只能运行一个实例程序,这时候可以使用文件和记录锁(简称文件锁)来实现这个功能。
守护进程只要创建一个固定名字的文件(一般在 /var/run 目录中),并在该文件整体上加一把写锁,那么此后其他的守护进程如果想要给该文件加锁就会失败,也就不应该继续运行。在守护进程终止时,锁会被自动删除,简化了复原过程。
线程
线程介绍
线程包含了表示进程内执行环境的必须信息,其中包含进程中表示线程的线程ID,一组寄存器值,栈,调度优先级和策略。信号屏蔽字,errno值以及线程私有数据。
进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本,程序的全局内存和堆内存,栈以及文件描述符。
- 线程标识
就像每一个进程都有一个进程ID一样,每一个线程也有一个线程ID,进程ID在整个系统中是唯一的,但线程ID不同,线程ID只在它所属的进程环境中有效。线程ID用pthread_t数据类型来表示,(Linux使用无符号长整数表示pthread_t结构)。实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的系统不能把它当做整数来处理,因此必须使用函数来对两个线程ID进行比较。
线程可以通过pthread_self函数获取自身的线程ID
1 |
|
pthread_create
线程创建
1 | pthread_t pthread_create(pthread_t *tidp,const pthread_attr_t *attr,void *(*start_rtn)(void),void * arg)//若成功返回0,否则返回错误编号 |
新创建的线程的线程 ID 被设置成 tidp 指向的内存单元;attr 参数定制线程的不同属性;start_rtn 函数是线程开始时执行的函数,其参数可以通过 arg 进行传递。
注意:新线程最好不要通过 tidp 指向的内存空间获取自己的线程 ID,因为如果新线程在主线程调用 pthread_create 返回前就运行了,那么它看到的就是未经初始化的内容,很可能并不是正确的线程 ID。可以使用 pthread_self 函数获取自己的线程 ID。
pthread_exit
- 线程退出
如果进程中某一个线程调用了exit,_exit或者_Exit,那么整个进程就会终止。类似的,如果信号的默认动作是终止进程,那么把该信号发送到某个线程,整个进程都会终止。
单个线程可以有三种方式退出:
1.直接从启动实例中返回,返回值是线程退出码
2.被同一进程的其他线程取消
3.调用 pthread_exit
1 |
|
pthread_join
1 |
|
rval_ptr是一个无类型指针,进程中的其他线程可以通过调用pthread_join函数访问到这个指针
调用 pthread_join 的线程会一直阻塞,直到指定的线程终止。如果指定的线程直接返回或者是调用 pthread_exit 终止,则可以通过 rval_ptr 查看其返回值;如果线程是被取消的,则 rval_ptr 被设置为 PTHRERAD_CANCELED。
如果线程已经处于分离状态,那么,pthread_join调用就会失败。
pthread_create和pthread_exit函数的无类型指针参数能传递的数值不止一个,该指针可以传递更复杂信息的结构地址,但是注意这个结构所使用的内存,在调用者完成调用以后必须仍然是有效的,否则就会出现无效或者非法内存访问。例如在调用线程的栈上分配了该结构,那么其他线程在使用这个结构时内存可能就已经改变了。(可以使用全局栈结构malloc调用分配结构)。
pthread_cancel
- 取消线程
1 | // Returns: 0 if OK, error number on failure |
线程可以安排退出时它调用的函数,这样的函数成为线程清理处理程序。处理程序记录在栈中,也会就是说他们的执行顺序与注册时相反。
线程清理
- 线程清理处理程序
1 | void pthread_cleanup_push(void (*rtn)(void *), void *arg); |
清理函数 rtn 只有在以下情况会执行:
1.调用 pthread_exit
2.响应取消请求
3.用非零execute 参数调用 pthread_cleanup_pop(为 0 时,清理函数不会被调用)
两个函数需要成对使用。
pthread_detach
- 线程分离
1 | // Returns: 0 if OK, error number on failure |
默认情况下,线程的终止状态会保留,直到调用 pthread_join。如果线程被分离,则资源会在线程终止后被立即收回。
线程同步
互斥量mutex互斥锁
1 | // All return: 0 if OK, error number on failure |
互斥变量为 pthread_mutex_t 类型,如果使用静态分配方式,可以直接使用 PTHREAD_MUTEX_INITIALIZER 进行初始化。对于动态分配的互斥量,在释放内存前需要调用 pthread_mutex_destroy。
- 带有超时的互斥锁
1 |
|
如果不希望线程在访问加锁的互斥量时无限等待,可以通过 pthread_mutex_timedlock 指定等待的绝对时间。
1 |
|
读写锁rwlock
读写锁有 3 中状态:不加锁、读模式加锁和写模式加锁。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
读写锁适合对数据结构读的次数远大于写的情况。
1 | int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, |
条件变量cond
当线程等待的条件变量被满足后,该线程就会被唤醒。条件变量需要和互斥量配合使用,条件本身是由互斥量保护的。
在使用条件变量之前,必须对其进行初始化(有静态和动态 2 种方式)。
1 | // All return: 0 if OK, error number on failure |
pthread_cond_wait 操作主要执行如下操作步骤
- 解锁互斥量 mutex
- 阻塞调用线程,直至另一线程就条件变量 cond 发出信号
- 重新锁定 mutex
在使用 pthread_cond_wait 函数之前,应该已经取得 mutex 锁。对 pthread_cond_wait 的调用应该放在 while 循环中,因为从 wait 函数返回时,并不能确定条件已经得到满足(其他线程先醒来、虚假唤醒等),需要重新对条件进行判断。
1 | // 消费者进程 |
自旋锁spin
自旋锁与互斥量大体类似,主要的不同之处在于自旋锁在获取锁之前会一直忙等。因此,使用自旋锁应该保证持有锁的时间很短。
自旋锁和互斥量的接口类似
1 | // All return: 0 if OK, error number on failure |
pshared 表示进程共享(process-shared)属性,表明自旋锁的获取方式。它仅在支持线程进程共享同步(Thread Process-Shared Synchronization)的平台上有效,当设置为 PTHREAD_PROCESS_SHARED,则只要线程可以访问锁底层内存,即使是不同进程的线程都可以获得锁;而设置为 PTHREAD_PROCESS_PRIVATE 后,只有初始化该锁的进程内部的线程可以访问它。
barrier
屏障允许多个线程等待,直到所有合作线程满足某个点后,从该点继续执行。主线程可以将某个任务分解多个小任务交给不同的线程,等到所有线程工作完成后,主线程在此基础上继续执行。
使用 8 个线程分解 800 万个数的排序工作,每个线程对其中的 100 万个数排序,最后由主线程将这些结果进行合并。
1 | // Both return: 0 if OK, error number on failure |
wait 函数表明当前线程已完成工作,准备等待其他线程。当线程调用该函数后满足屏障计数,那么函数的返回值为 PTHREAD_BARRIER_SERIAL_THREAD,其余线程该函数返回值为 0。这一特点使得可以很容易的将一个线程作为主线程,它可以工作在其他所有线程已完成的工作结果上。
1 |
|
线程控制
线程属性和同步原语属性,基于进程的系统调用如何与线程进行交互。
管理这些属性的函数大概有以下几类:
1.初始化函数,负责给属性设置为默认值
2.销毁函数,负责释放初始化函数分配的资源
3.获取属性值的函数
4.设置属性值的函数
线程属性
1 | // Both return: 0 if OK, error number on failure |
同步属性
互斥锁属性
1 | // Both return: 0 if OK, error number on failure |
PTHREAD_MUTEX_NORMAL :标准互斥量,不进行错误检查或死锁检测。
PTHREAD_MUTEX_ERRORCHECK :提供错误检查
PTHREAD_MUTEX_RECURSIVE :允许同一线程在解锁前多次加锁。
PTHREAD_MUTEX_DEFAULT :提供默认的特性和行为,操作系统可以将其映射为其他类型。
Mutex type Relock without unlock? Unlock when not owned? Unlock when unlocked?
PTHREAD_MUTEX_NORMAL deadlock undefined undefined
PTHREAD_MUTEX_ERRORCHECK returns error returns error returns error
PTHREAD_MUTEX_RECURSIVE allowed returns error returns error
PTHREAD_MUTEX_DEFAULT undefined undefined undefined
读写锁属性
读写锁非常适合于对数据结构读的次数远大于写的情况
1 | // All return: 0 if OK, error number on failure |
条件变量属性
1 | // All return: 0 if OK, error number on failure |
barrier属性
1 | // All return: 0 if OK, error number on failure |
线程特定数据
线程模型促进了进程中数据和属性的共享,但是在部分场景下,我们又希望线程的部分数据可以是私有的。
一个进程中的所有线程都可以访问进程的整个地址空间,因此线程没有办法阻止另一个线程访问它的数据(除非使用寄存器),即使是接下来介绍的线程特定数据(thread-specific data)机制,也不能做到这一点。但是通过这种机制,可以提高线程间的独立性,使得线程不太容易访问到其他线程的线程特定数据。
每个线程通过 键(key) 来访问线程特定数据,键在进程中被所有线程使用,每个线程把自己的线程特定数据和键关联起来。这样,通过同一个键,每个线程可以管理与自己关联的数据。
1 | // Both return: 0 if OK, error number on failure |
创建新键时,每个线程的数据地址为空。同时,在创建的时候可以指定一个析构函数,当线程退出时,如果数据地址不为空,则会调用这个析构函数(参数是数据地址)。
所有的线程都可以调用删除函数来取消键与数据之间的关联,但是这不会触发析构函数。
1 | // Returns: thread-specific data value or NULL if no value has been associated with the key |
- 取消选项
有 2 个额外的线程属性并没有包含在上述的 pthread_attr_t 中,它们分别是可取消状态和可取消类型。
1 | // Returns: 0 if OK, error number on failure |
如果将状态设置为 PTHREAD_CANCEL_DISABLE,那么调用 pthread_cancle 函数并不会杀死线程,取消请求会一直处于挂起状态,直到状态被设置为 ENABLE。同理,此时调用 pthread_testcancel 没有任何效果。
1 | // Returns: 0 if OK, error number on failure |
默认设置为 PTHREAD_CANCEL_DEFERRED,即推迟取消,线程到达取消点之前不会被真正取消。如果设置为 PTHREAD_CANCEL_ASYNCHRONOUS,即异步取消,那么线程可以在任意时间撤销,而不必等待到达取消点
线程和信号
每个线程有自己的信号屏蔽字,通过 pthread_sigmask 函数进行设置,参数与 sigprocmask 类似
1 |
|
1 | int main() |
在多线程中,一般安排专用线程处理信号,通过互斥量的保护,信号处理线程可以安全地改动数据。
线程和fork
线程调用 fork 时,为子进程创建了整个进程地址空间的副本,同时还继承了互斥量、读写锁和条件变量的状态。为此,子进程返回后,如果不是马上调用 exec,则需要清理锁的状态。因为子进程中只含有调用 fork 的那个线程的副本,父进程中其他占有锁的线程在子进程中不存在。
要清除锁的状态,可以使用 pthread_atfork 函数建立 fork 处理程序。
1 | // Returns: 0 if OK, error number on failure |
prepare 由父进程在 fork 创建子进程前调用。任务是获取父进程定义的所有锁。
parent 在 fork 创建子进程后、返回之前在父进程上下文中调用。任务是对获取的所有锁进行解锁。
child 在 fork 返回前在子进程上下文中调用。任务是释放所有的锁。
可以多次调用该函数以设置多套 fork 处理程序。对于不需要的某个处理程序,可以传入空指针。多次调用时,parent 和 child 以注册时的顺序执行,而 prepare 的执行顺序与注册时相反。
进程间通信IPC
管道pipe-半双工(高级管道popen、有名管道FIFO-半双工)、消息队列、信号量(sem,用来控制多个进程对共享资源的访问,常作为一种锁机制,同步手段)、信号(signal)、共享存储(结合信号量)、套接字socket
FIFO命名管道
管道
一般来说,管道是半双工的(即数据只能在一个方向上流动),并且只能在具有公共祖先的两个进程之间使用。通常,父进程创建管道后会接着调用 fork,从而利用管道在父子进程之间通信。
父子进程可以分别关闭管道的读 / 写端,以利用管道在父子进程中传递信息。例如,如果想要创建从父进程到子进程的管道,则可以关闭父进程的读端和子进程的写端。由于管道半双工的特性,想要在父子进程间双向传递信息需要建立 2 个管道。
创建管道
1 |
|
fd 参数返回两个文件描述符,fd[0] 为读而打开,fd[1] 为写而打开。fd[1] 的输出是 fd[0] 的输入。
读写管道规则
当读一个写端被关闭的管道,在所有数据被读取后,read 返回 0
当写一个读端被关闭的管道,会产生 SIGPIPE 信号。如果忽略该信号或从信号处理程序返回,则 write 返回 - 1,且设置 errno 为 EPIPE
写入不超过 PIPE_ BUF 字节的操作是原子的,如果写入数据的大小超过该值,在多个进程同时写一个管道时,所写的数据可能交叉
popen/pclose
管道的通常用法是创建一个连接到另一个进程的管道,然后读取其输出或者向其输入端发送数据。可以使用 popen 和 pclose 实现这一功能。
这两个函数实现的操作是:创建一个管道,fork 一个子进程,关闭未使用的管道,执行 shell 运行命令,然后等待命令终止。
1 |
|
popen 先执行 fork,然后调用 exec 执行 cmdstring,并且返回一个标准 I/O 文件指针。
如果 type 是 “r”,则文件指针连接到 cmdstring 的标准输出,如果是 “w” 则连接到标准输入。cmdstring 以 sh -c cmdstring 的方式执行。pclose 函数关闭标准 I/O 流,等待命令终止,然后返回 shell 的终止状态。(注意不要使用 fclose 函数,它不会等待子进程结束)。
FIFO命名管道
管道只能用于两个进程之间线性连接,FIFO有名字,可用于非线性连接。使得不相关的进程间也能交换数据,也是一种文件类型,创建 FIFO 与创建文件类似,需要指定其路径。
FIFO
是一种文件类型,通过stat
结构的st_mode
成员的编码可以知道文件是否是FIFO
类型。可以用S_ISFIFO
宏对此进行测试
FIFO有以下2个用途:
- shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件
- 客户进程-服务器进程应用程序中,FIFO用作汇聚点(多个客户进程向服务器同一个众所周知的FIFO写),在客户进程和服务器进程二者之间传递数据
创建FIFO
1 |
|
mkfifo:mode 参数指明 FIFO 的文件权限,与 open 函数中的 mode 相同。
mkfifoat:path 参数有如下几种情况:
- 如果指定为绝对路径,则会忽略 dirfd 参数,行为与 mkfifo 类似
- 如果指定为相对路径,则该路径与 dirfd 打开的目录有关
- 如果指定为相对路径,且 dirfd 有参数 AT_FDCWD,那么路径以当前目录开始
应用程序可以用
mknod
和mknodat
函数创建FIFO。因为POSIX.1原先并没有包括mknod
函数,所以mkfifo
是专门为POSIX.1设计的。mknod
和mknodat
函数现在已包括在POSIX.1的XSI扩展中
打开FIFO
可以使用 open 打开 FIFO
- 一般情况下,在打开时如果没有设置非阻塞标志 O_NONBLOCK,只读(O_RDONLY)
open
要阻塞到某个其它进程为写而打开这个FIFO为止。类似地,只写(O_ WRONL )open
要阻塞到某个其它进程为读而打开它为止
但是,不应该使用 O_RDWR 的方式来绕过这种阻塞行为,而应该使用非阻塞标志。使用读写方式打开 FIFO,会导致读取数据时永远看不到文件结束,因为至少会有一个写描述符是打开着的。
读写FIFO
- 若
write
一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE
- 若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志
一个给定的FIFO有多个写进程是常见的。这意味着,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作。和管道一样,常量PIPE_BUF
说明了可被原子地写到FIFO的最大数据量。
使用 FIFO 进行客户进程与服务器进程之间的通信。每个客户进程可以将自己的请求写到一个公共的 FIFO 文件中(请求长度需要小于 PIPE_BUF 以避免客户进程之间的数据交叉),服务器进程针对每个客户进程创建 FIFO,用于向客户进程发送数据。客户进程的 FIFO 的路径名可以使用客户进程的 PID 号作为基础,如 /tmp/servv1.PID,这样客户进程就知道该从哪个 FIFO 读取服务器进程返回的数据了。
XSI-IPC
每个 IPC 对象与键(key)相关联,以使得多个进程可以通过它进行联系。在创建 IPC 结构时,必须指定一个键。而在系统内部,则使用标识符引用 IPC 结构。——消息队列、信号量、共享内存。
XSI-IPC介绍
标识符和键
标识符
- 每个内核中的IPC结构(消息队列、信号量或共享内存)都用一个非负整数的标识符加以引用
- 与文件描述符不同,IPC标识符不是小的整数。当一个IPC结构被创建,然后又被删除时,与这种结构相关的标识符连续加1,直至到达一个整形数的最大正值,然后又回转到0
键
- 标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个IPC对象都与一个键相关联,将这个键作为该对象的外部名(创建IPC结构时,应指定一个键)。键的类型是基本系统数据类型
key_t
,通常在<sys/types.h>
中被定义为长整形。这个键由内核变换成标识符。
权限和结构
每个IPC结构关联了一个ipc_perm
结构(<sys/ipc.h>
),规定了权限和所有者,至少包括以下成员
1 | struct ipc_perm{ |
- 创建IPC结构时,对所有字段都赋初值
- IPC结构的创建者或超级用户可以调用
msgctl
、semctl
或shmctl
修改uid
、gid
和mode
字段。修改这些字段类似于对文件调用chown和chmod
对于任何IPC结构都不存在执行权限,消息队列和共享内存使用术语”读“和”写“,信号量则用”读“和”更改“。
键的创建方式,主要有如下几种:
指定为 IPC_PRIVATE,这会创建一个新的 IPC 结构,可以将返回的标识符存入文件供其他进程使用,也可直接给 fork 后的子进程使用
在公共头文件中定义一个键,然后由一个进程(通常是服务器进程)根据这个键来创建新的 IPC 结构。但是这种方式可能会与已经存在的键冲突,需要进程删除原有的 IPC 结构再重新创建。
使用 ftok 函数,将路径名和某个数字(0-255)变换为一个键。
key_t ftok(const char *path, int id); // Returns: key if OK, (key_t)−1 on error
path 参数必须引用的是现有的文件,id 参数只使用其低 8 位。
在创建 IPC 结构时还需要指定其权限,与文件权限类似,但是不存在执行权限。
注意:
- IPC_PRIVATE 只能用于创建新的 IPC 结构,而不能用来引用一个现有的 IPC 结构。
- 如果希望确保新创建的 IPC 结构没有引用具有同一标识符的现有 IPC 结构,则可以在 flag 中同时指定 IPC_CREAT 和 IPC_EXCL。这样,如果已经存在则会返回 EEXIST。
优缺点
在系统范围内起作用,没有引用计数
- 如果创建一个消息队列,放入消息后终止,消息队列和内容不会删除,直到调用msgrcv或msgctl读取或删除消息队列,或者ipcrm删除消息队列或自举系统删除消息队列,
在文件系统重没有名字
- 不能用ls查看IPC对象
- 不能对它们使用多路转接I/O函数(select-poll)
消息队列
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。以下简称队列。相关的数据结构很少用到,后面的信号量和共享存储同理。
结构
每个队列都有一个msqid_ds
结构与其关联,这个结构定义了队列的当前状态
1 | struct msqid_ds{ |
最大消息队列数,消息队列最大容量,一个消息最大长度8192。
linux 消息队列的限制 ipcs -q
创建/打开消息队列
msgget 用于创建或打开一个队列
1 |
|
key 参数可以是通过 ftok 函数生成的,也可以是 IPC_PRIVATE。flag 用于设定读写权限,如果是新建该 IPC 结构则可以添加 IPC_CREAT。
添加到队列
将新消息添加到队列尾端
1 | // Returns: 0 if OK, −1 on error |
flag 可以指定为 IPC_NOWAIT,当消息队列满时(或达到系统限制),会立即出错返回 EAGAIN。
否则,进程会一直阻塞直到:有空间容纳消息;队列被删除(返回 EIDRM);或捕捉到信号并从处理程序返回(返回 EINTR)。
获取消息
用于从队列中取出消息,可以指定获取某些类型的数据,而不是必须按照先进先出的次序。
1 | ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag); |
type==0:返回队列中的第一个消息
type>0:返回消息类型为 type 的第一个消息
type<0:返回消息类型≤type 绝对值的消息,如果有若干个满足则取类型最小的。
flag 参数同样可以指定为非阻塞
操作消息队列
1 | int msgctl(int msqid, int cmd, struct msqid_ds *buf ); |
对队列执行多种操作,msqid 队列ID(标识符),msgget
的返回值
cmd 参数指定队列需要执行的操作:
- IPC_STAT:获取队列的 msqid_ds 结构信息,存放于 buf 指向的结构中
- IPC_SET:将 msg_perm.uid,msg_perm.gid,msg_perm.mode 和 msg_qbytes 通过 buf 复制到队列的 msqid_ds 结构中。该命令只能由超级用户或者有效用户 ID 等于 msg_perm.cuid 或 msg_perm.uid 的用户执行。
- IPC_RMID:删除队列及其中的数据。也只能由上述的两类用户执行。
这 3 条命令也适用与信号量(semctl)和共享存储(shmctl)。
信号量
信号量是一个计数器,用于为多个进程提供对共享数据对象的访问。创建的时候需要指明信号量的个数,在使用的时候也要指明用的是哪个信号量。
1 |
|
sem_flg 的 SEM_UNDO 标志标识当进程终止时,该操作修改的信号量值会被恢复,即重新设置为调用该操作之前的数值。
sem_op 可以指定如下 3 种值:
正值,表示进程释放的占用的资源数,sem_op 值会加到对应的信号量的值上。
0,表示进程希望等待该信号量值变为 0。IPC_NOWAIT 标志可以控制进程是否阻塞,相关的出错返回信息可以查阅手册,此处省略。
负值,表示进程想要获取的资源数。如果信号量值≥sem_op 的绝对值(满足需求),则会从当前的信号量值上减去对应的值,否则由 IPC_NOWAIT 标志决定进程是否阻塞。
semop 函数具有原子性,即要么执行数组中所有的操作,要么什么也不做。
共享存储
共享存储允许两个或多个进程共享一个给定的存储区。但是,需要注意存储区访问的同步问题,当进程在写入数据时其他进程不应该去读取这些数据。一般使用信号量来解决这一同步问题。
相比与通过文件映射的方式来共享存储区的方式,XSI 共享存储没有相关的文件,它共享的是内存的匿名段。
mmap就是共享存储的一种形式,但是XSI共享存储与其区别在于,XSI共享存储没有相关文件。XSI共享存储段是内存的匿名段
1 |
|
shmat 用于将共享存储段连接到进程的地址空间。具体连接到地址空间的什么位置由 2、3 两个参数决定。
1.addr=0,则连接到内核选择的第一个可用地址上。(推荐)
2.addr≠0,且 flag 没有指定 SHM_RND,那么连接到 addr 指定的地址。
3.addr≠0,且指定了 SHM_RND,那么系统会按照公式 (addr-(addr % SHMLBA)) 决定连接地址。该公式作用是将地址向下取最近的 SHMLBA 的倍数,而常数 SHMLBA 表示 “低边界地址倍数”。
flag 还可以指定 SHM_RDONLY 以只读方式连接共享段。
POSIX信号量
POSIX 信号量与 XSI 信号量最大的不同就是没有信号量集的概念,一次只能操作一个信号量。还有就是在删除信号量时,正在使用 XSI 信号量的操作会失败;而 POSIX 信号量的操作会正常执行,直到该信号量的最有一个引用被释放。
POSIX 信号量有两种形式:命名的和未命名的。两者的差异在于创建和销毁的形式上,使用的方式是一样的。未命名的信号量只存在于内存中,因此想要使用这些信号量的进程需要有对应的访问权限,如同一进程中的线程,或者是不同进程中映射相同的内存内容到自己的地址空间的线程。而命名信号量可以被任何直到它们名字的进程访问。
命名信号量:名字的第一个字符应该是 /。因为一般 POSIX 信号量的实现要使用文件系统;名字不应该包含其他斜杠;名字长度是实现定义的,不应长于_POSIX_NAME_MAX。
创建/获取信号量
用于创建一个新的信号量或使用一个现有的信号量 oflag可为0
1 |
|
当 oflag 包含 O_CREAT 标志时,如果信号量不存在则会创建新的,如果存在则会被使用,但不会重新初始化。指定此标志时,还需要提供后面的 2 个参数。mode 指定访问权限,这与打开文件的权限相同;value 指定信号量的初值。
如果 oflag 同时指定了 O_EXCL 标志,则在创建信号量时,如果信号量已经存在就会出错
- 使用现有的命名信号量时,仅指定2个参数:
name
:信号量的名字oflag
:设为0
- 创建新的命名信号量
name
:信号量的名字oflag
:指定了O_CREAT
标志。当该参数置为O_CREAT|O_EXCL
并且信号量存在时,函数会失败mode
:谁可以访问信号量,值与open函数的权限位相同value
:信号量的初始值(0~SEM_VALUE_MAX
)
为了移植性,信号量的命名应该遵循下列规则:
- 名字的第一个字符应该为斜杠(
/
) - 名字不应该包含其他斜杠以此避免实现定义的行为
- 信号量名字的最大长度是实现定义的,不应该鲳鱼
_POSIX_NAME_MAX
个字符长度。因为这是文件系统的实现能允许的最大名字长度的限制
关闭信号量
1 | int sem_close(sem_t *sem); |
sem_close关闭一个信号量,释放相关资源, 进程退出时如果没有调用该函数,系统也会自动关闭打开的信号量。POSIX 信号量没有 UNDO 机制,所以信号量的值不会受到影响
销毁信号量
1 | int sem_unlink(const char *name); |
删除信号量的名字,如果没有打开的信号量引用,信号量会被立即销毁,否则会延迟到最后一个打开的引用关闭
信号量操作
与 XSI 信号量不同,POSIX 信号量一次操作只能 + 1 或者 - 1
1 |
|
如果在多个进程间共享一个资源,则可使用3种技术中的一种来协调访问,可以使用映射到两个进程地址空间中的信号量、记录锁或者(共享存储中的)互斥量,(共享存储中的)互斥量更快,但是记录锁简单也快
未命名信号量
主要用于单个进程
1 | // 创建一个未命名信号量,value 指定其初值,pshared 值为 0 时,信号量仅在进程的线程之间共享;不为 0 则表明会在进程之间共享 |
套接字
1 |
|
int domain : 用于确定网络类型
int type : 用于确定协议类型
int protocol : 用于消歧义,决定网络协议,通常是0
参考:Unix高级编程笔记