fork系统调用
Linux下创建新进程
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
在父进程中返回子进程PID,子进程中返回0
失败返回-1并设置errno
复制进程映像,在内核进程表中创建新的进程表项。子进程代码与父进程完全相同,同时子进程会复制父进程的数据(堆数据、栈数据、静态数据)
新进程表中与原进程相同的属性:
- 堆指针
- 栈指针
- 标志寄存器的值
数据的复制采用写时复制【只有任一进程对数据进行写操作时才会复制】
创建子进程后父进程中打开的文件描述符默认在子进程中也是打开,文件描述符的引用计数会加一,且父进程的用户根目录、当前工作目录等变量引用计数也加一
exec系列系统调用
替换进程映像需要在子进程中执行其他程序【原程序被exec指定的程序完全替换,其中包括代码、数据】
#include <unistd.h>
extern char** environ;
int execl(const char* path,const char* arg,...);
int execlp(const char* file,const char* arg,...);
int execle(const char* path,const char* arg,...,char* const envp[]);
int execv(const char* path,char* const argv[]);
int execvp(const char* file,char* const argv[]);
int execve(const char* file,char* const argv[],char* const envp[]);
- path:指定可执行文件路径
- file:接受文件名,文件位置在环境变量PATH中寻找
- arg:接受可变参数
- argv:接受参数数组,将传递给新程序【path/file未指定】的main函数
- envp:设置新的环境变量,如果不设置,新程序使用environ指定的环境变量
- 一般不返回,出错时返回-1并设置errno
exec系列函数不会关闭原程序打开的文件描述符,除非该文件描述符设置了如SOCK_CLOSEEXEC的属性
处理僵尸进程
僵尸进程:
- 子进程结束而父进程没有注意到【子进程结束运行后,都进程读取其退出状态前】
- 父进程结束运行/异常终止,子进程继续运行(此时子进程PPID被OS设为1,init进程接管该进程并等他结束)
以上:若父进程没有正确处理子进程的返回信息,子进程将停留在僵尸态、占据内核资源。
避免/使僵尸态结束:
//父进程中调用,等待子进程退出并获取其返回信息
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
pid_t waitpid(pid_t pid,int* stst_loc,int options);
- wait阻塞进程,直到进程中某子进程运行结束,返回结束运行的子进程PID,将退出信息存在stat_loc指向的内存中。
sys/wait.h中定义宏解释子进程退出信息:
- waitpid只等待pid指定的子进程,(pid==-1时,等待任意子进程退出)。options控制函数的行为,常取WNOHANG【表示非阻塞:子进程正常退出,返回子进程PID;子进程没有结束/异常终止,立刻返回0;调用失败返回-1并设置errno】
使用:在事件结束后使用非阻塞调用以提高效率即子进程退出后使用。当进程结束后会给父进程发送SIGCHLD信号,可以在父进程中捕获该信号并在信号处理函数中调用waitpid从而彻底清理子进程
//SIGCHLD信号处理函数
static void handle_child(int sig)
{
pid_t pid;
int atat;
while((pid=waitpid(-1,&stat,WNOHANG))>0)
{
//处理子进程
}
}
管道
既是进程间内部通信方式,也可实现父子进程间通信
调用fork后两管道文件描述符都打开。一对这样的文件描述符保证一个方向上的数据传输(父子进程必须一个关闭fd[0]、一个关闭fd[1])
匿名管道仅用于有关联的两进程间通信
命名管道【FIFO先进先出——一种特殊管道】可用于无关联进程间通信
信号量
当多个进程同时访问系统上的某个资源的时候,需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。
通常,程序对共享资源的访问的代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。我们称这段代码为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。
Dijkstra提出信号量是并发编程领域的重要一步。
信号量是一个特殊的变量,只能取自然数值且只支持两种操作【等待、信号】,在Linux/UNIX中将这两种操作称为:P、V【passeren—传递,进入临界区,vrijgeven—释放,退出临界区】
如有一信号量SV,P、V操作含义:
- P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行
- V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将sv加1
信号量可取任何自然数,常用的信号量是二进制信号量【只能取0/1】
eg:
SV初始值为1,进程AB都想访问关键代码段,如果A执行了P将SV-1,进程B访问时挂起,直到A执行V将SV+1,关键代码段重新可用,这时发现B因等待该关键代码段被挂起,将其唤醒并进入关键代码段。
Linux信号量的API定义在sys/sem.h头文件中,主要包含三个系统调用:semget、semop、semctl【可操作信号量集】
semget系统调用
创建一个新的信号量集或获取已经存在的信号量集:
#include <sys/sem.h>
int semget(key_t key,int num_sems,int sem_flags);
-
key是一个键值,用来标识一个全局唯一的信号量集,就像文件名全局唯一地标识一个文件一样。要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量
-
num_sems指定要创建/获取的信号量集中信号量的数目。如果是创建信号量,必须指定;如果是获取已经存在的信号量,可以设为0
-
sem_flags参数指定一组标志。低端的9个比特是该信号量的权限,格式和含义与系统调用open的mode参数相同。它还可以和IPC_CREAT标志做按位“或”运算以创建新的信号量集,此时即使信号量已经存在,semget也不会产生错误;还可以联合使用IPC_CREAT和IPC_EXCL标志来确保创建一组新的、 唯一的信号量集,在这种情况下,如果信号量集已经存在,则semget返回错误并设置errno为EEXIST。这种创建信号量的行为与用O_CREAT和O_EXCL标志调用open来排他式地打开一个文件相似。
semget成功时返回一个正整数值,它是信号量集的标识符:semget 失败时返回-1,并设置ermno。如果semget用于创建信号量集,则与之关联的内核数据结构体semid_ds将被创建并初始化。
semid_ds 结构体:
#include <sys.sem.h>
//用于描述IPC对象(信号量、共享内存、消息队列)的权限
struct ipc_perm
{
key_t key; //键值
uid_t uid; //所有者有效用户ID
gid_t gid; //所有者有效组ID
uid_t cuid; //创建者有效用户ID
gid_y cgid; //创建者有效组ID
mode_t mode; //访问权限
};
struct semid_ds
{
struct ipc_perm sem_perm;//信号量的操作权限
unsigned long int sem_nsems;//信号量集中的信号量数目
time_t sem_otime;//最后调用semop时间
time_t sem_ctime;//最后调用semctl时间
};
semget对semid_ds结构的初始化包括:
- 将sem_perm.cuid和sem_perm.uid设置为调用进程的有效用户ID
- 将sem_perm.cgid和sem_perm.gid 设置为调用进程的有效组ID
- 将sem_perm.mode的最低9位设置为sem_flags参数的最低9位
- 将sem_nsems设置为num_sems
- 将sem_otime设置为0
- 将sem_ctime设置为当前的系统时间。.
semop系统调用
执行P、V操作,改变信号量的值。
一些与信号量关联的重要内核变量:
unsigned short semval;//信号量的值
unsigned short semzcnt;//等待信号量值变成0的进程数量
unsigned short semncnt;//等待信号量值增加的进程数量
pid_t sempid;//最后一次执行semop操作的进程
semop对信号量的操作其实就是对上面内核变量的操作。
#include <sys/sem.h>
int semop(int sem_id,struct sembuf* sem_ops,size_t num_sem_ops);
- sem_id 是由semget调用返回的信号量集标识符,用以指定被操作的目标信号量集
- sem_ops参数指向一个sembuf结构体类型的数组
sembuf定义:
struct sembuf
{
unsigned short int sem_num;//信号量编号
short int sem_op;//指定操作类型
short int sem_flg;//影响操作类型的行为
//sem_flg可选IPC_NOWAIT:无论操作是否成功,都会立即返回【类似非阻塞IO】
//SEM_UNDO:进程退出时取消正在进行的semop操作
};
- num_sem_ops指定要执行的操作个数【sem_ops中元素个数】
- semop对sem_ops中每个成员按照数组顺序依次执行操作,该过程是原子操作以避免别的进程在同时按不同顺序对信号集的信号执行senop操作导致的竞态条件
- 成功返回0,失败返回-1并设置errno且失败时sem_ops数组中指定的所有操作都不执行
semctl系统调用
允许调用者对信号量直接进行控制
#include <sys/sem.h>
int semctl(int sem_id,int sem_num,int command,...);
- sem_id:由semget调用返回的信号量集标识符,用以指定被操作的信号量集
- sem_ num:指定被操作的信号量在信号量集中的编号
- command :指定要执行的命令:
- 有的命令需要调用者传递第4个参数。第4个参数的类型由用户自己定义,sys/sem.h头文件给出了它的推荐格式:
union semun
{
int val; //用于SETVAL命令
struct semid_ds* buf;//用于IPC_ STAT和1PC_ SET命令
unsigned short" array;//用于GETALL和SETALL命令
struct seminfo* __buf;//用于IPC_ INFO命令
};
struct seminfo
{
int semmap;//Linux内核没有使用
int semmni;//系统最多可以拥有的信号量集数目
int semmns;//系统最多可以拥有的信号量数目
int semmnu;//没有使用
int semmsl;//一个信号量集最多允许包含的信号量数目
int semopm;//semop一次最多执行的sem_op操作数
int semume;//没有使用
int semusz;//sem_undo结构体大小
int semvmx;//最大允许的信号量值
int semaem;//最多允许的带SEM_UNDO标志的semop操作次数
};
- 失败时返回-1并设置errno
特殊键值ipc_private
semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE (其值为0),这样无论该信号量是否已经存在,semget 都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字声称的那样是进程私有的。其他进程,尤其是子进程,也有方法来访问这个信号量。所以semget的man手册的BUGS部分上说,使用名字IPC_PRIVATE有些误导(历史原因),应该称为IPC_NEW
共享内存
共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输
这种高效率带来的问题是,必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用。
含四个系统调用:shmget,shmat,shmctl
shmget系统调用
创建一段新的共享内存或获取已经存在的共享内存
#include <sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
- key是一个键值,用来标识一段全局唯一的共享内存
- size指定共享内存的大小,单位是字节。如果是创建新的共享内存,则size值必须被指定;如果是获取已经存在的共享内存,可把size设为0
- shmflg参数的使用和含义与semget系统调用的sem. fags 参数相同。不过shmget支持两个额外的标志一SHM_HUGETLB和SHM_NORESERVE,含义:
SHM_HUGETLB,类似于mmap的MAP_HUGETLB标志,系统将使用大页面来为共享内存分配空间
SHM_NORESERVE,类似于mmap的MAP_NORESERVE标志,不为共享内存保留交换分区(swap)。这样,当物理内存不足的时候,对该共享内存执行写操作将触发SIGSEGV信号 - shmget成功时返回一个正整数值,它是共享内存的标识符。shmget 失败时返回-1,并设置ermo.
如果shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0,与之关联的内核数据结构shmid_ds将被创建并初始化
shmat和shmdt系统调用
共享内存被创建/获取之后,不能立即访问,而是需要先将它关联到进程的地址空间中。
使用完共享内存之后,需要将它从进程地址空间中分离。
这两项任务分别由如下两个系统调用实现:
#include <sys/shm.h>
void* shmat(int shm_id,const void* shm_addr,int shmlg);
int shmdt(const void* shm_addr);
shm_id是由shmget调用返回的共享内存标识符
shm_addr指定将共享内存关联到进程的哪块地址空间,最终的效果受到shmfg参数的可选标志SHM_RND的影响:
- shm_addr为NULL:则被关联的地址由操作系统选择。这是推荐的做法,以确保代码的可移植性
- shm_addr非空:SHM_RND标志未设置,共享内存被关联到addr指定的地址处
- shm_ addr 非空:设置了SHM_RND标志,被关联的地址是[shm_addr-(shm_addr%SHMLBA)]SHMLBA的含义是“段低端边界地址倍数”它必须是内存页面大小的整数倍。现在的Linux内核中,它等于一个内存页大小。SHM_RND 的含义是圆整,即将共享内存被关联的地址向下圆整到离shm_addr最近的SHMLBA的整数倍地址处。
除了SHM_RND标志外,shmfg参数还支持标志:
- SHM_RDONLY:进程仅能读取共享内存中的内容。若没有指定该标志,则进程可同时对共享内存进行读写操作(需要在创建共享内存的时候指定其读写权限)
- SHM_REMAP:如果地址shmaddr已经被关联到一段共享内存上,则重新关联
- SHM_EXEC:指定对共享内存段的执行权限。对共享内存而言,执行权限实际上和读权限是一样的
shmat成功时返回共享内存被关联到的地址,失败则返回(void)-1并设置erno. (成功时,将修改内核数据结构shmid_ds的部分字段)
shmctl系统调用
控制共享内存的某些属性:
#include <sys/shm.h>
int shmctl(int shm_id,int command,struct shmid_ds* buf);
shm_id是由shmget调用返回的共享内存标识符
command指定要执行的命令
shmctl 支持的所有命令:
失败时返回-1并设置errno
共享内存的posix方法
创建/打开一个POSIX共享内存对象:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open(const char* name,int oflag,mode_t mode);
name指定要创建/打开的共享内存对象。从可移植性的角度考虑,该参数应该使用“/somename"的格式:以“/"开始,后接多个字符,且这些字符都不是“/”;以“\0”结尾,长度不超过NAME_MAX (通常是255)
oflag指定创建方式。可以是下列标志中的一个或者多个的按位或:
- O_RDONLY.以只读方式打开共享内存对象
- O_RDWR.以可读、可写方式打开共享内存对象
- O_CREAT.如果共享内存对象不存在,则创建。此时mode参数的最低9位将指定该共 享内存对象的访问权限。共享内存对象被创建的时候,其初始长度为0
- O_EXCL和O_CREAT一起使用,如果由name指定的共享内存对象已经存在,则shm_open调用返回错误,否则就创建一个新的共享内存对象
- O_TRUNC.如果共享内存对象已经存在,则把它截断,使其长度为0。
shm_open调用成功时返回一个文件描述符,可用于后续的mmap调用,从而将共享内存关联到调用进程。shm_ open失败时返回-1,并设置ermo
和打开的文件最后需要关闭一样,由shm_open创建的共享内存对象使用完之后也需要被删除。实现:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcnt1.h>
int shm_unlink(const char *name);
//该函数将name参数指定的共享内存对象标记为等待删除。当所有使用该共享内存对象的进程都使用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源
如果代码中使用了POSIX共享内存函数,则编译的时候需要指定链接选项-lrt.
消息队列
消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式。每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,不一定像管道和命名管道那样必须以先进先出的方式接收数据
Linux消息队列的API包括4个系统调用: msgget、msgsnd、msgrev 和msgctl,都定义在sys/msg.h头文件中。
msgget系统调用
创建/获取一个消息队列
int msgget(key_t key,int msgflg);
key:标识一个全局唯一的消息队列
msgflg同semget中的sem_flags
成功返回一正整数值【消息队列的标识符】,失败返回-1并设置errno
如果是用来创建新的消息队列,内核数据结构msqid_ds将被创建并初始化
msgsnd系统调用
将一条信息加到消息队列中
int msgsnd(int msqid,const void* msg_ptr,size_y msg_sz,int msgflg);
msqid是由msgget调用返回的消息队列标识符
msg_ptr指向一个准备发送的消息,消息必须被定义为如下类型:
struct msgbuf
{
//mtype成员指定消息的类型,它必须是正整数
long mtype;//消息类型
char mtext[512]; //消息数据,可为0
};
msgflg控制msgsnd的行为。它通常仅支持IPC_NOWAIT标志,即以非阻塞的方式发送消息。默认情况下,发送消息时如果消息队列满了,则msgsnd将阻塞。若IPC_NOWAIT标志被指定,msgsnd将立即返回并设置errno为EAGAIN.
处于阻塞状态的msgsnd调用可能被如下两种异常情况所中断:
- 消息队列被移除。此时msgsnd调用将立即返回并设置erno为EIDRM
- 程序接收到信号。此时msgsnd调用将立即返回并设置erno为EINTR
msgsnd成功时返回0,失败则返回-1并设置errmo. msgsnd 成功时将修改内核数据结构msqid_ds的部分字段
msgrcv系统调用
从消息队列中获取消息
int msgrcv(int msqid,void* msg_ptr,size_t msg_sz,long int msgtype,int msgflg);
msqid是由msgget调用返回的消息队列标识符
msg_ptr用于存储接收的消息
msg_sz指的是消息数据部分的长度
msgtype指定接收何种类型的消息。可以使用如下几种方式来指定消息类型:
- msgtype等于0.读取消息队列中的第一个消息
- msgtype大于0.读取消息队列中第一个类型为msgtype的消息(除非指定了标志MSG_ EXCEPT)
- msgtype小于0.读取消息队列中第一个类型值比msgtype的绝对值小的消息
参数msgflg控制msgrev的行为。它可以是如下一些标志的按位或:
- IPC_NOWAIT.如果消息队列中没有消息,则msgrev调用立即返回并设置errno为ENOMSG
- MSG_EXCEPT. 如果msgtype大于0,则接收消息队列中第一个非msgtype类型的消息
- MSG_NOERROR.如果消息数据部分的长度超过了msg_sz, 就将它截断
处于阻塞状态的msgrev调用还可能被如下两种异常情况所中断:
- 消息队列被移除,此时msgrev调用将立即返回并设置errno为EIDRM
- 程序接收到信号,此时msgrev调用将立即返回并设置errno为EINTR
msgrev成功时返回0,失败则返回-1并设置ermo。(msgrev成功时将修改内核数据结构msqid_ ds的部分字段)
msgctl系统调用
控制消息队列的某些属性
int msgctl(int msqid,int command,struct msqid_ds* buf);
msqid是由msgget调用返回的共享内存标识符
command 参数指定要执行的命令,msgctl支持的所有命令:
icp命令
icps命令,观察当前系统上拥有的共享内存。
输出结果显示系统拥有的共享内存、信号量和消息队列资源
我们可以使用ipcrm命令来删除遗留在系统中的共享资源。
在进程间传递文件描述符
fork调用之后,父进程中打开的文件描述符在子进程中仍然保持打开,文件描述符可以很方便地从父进程传递到子进程。需要注意的是,传递一个文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创建一个新的文件描述符,并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项
那么如何在两个不相干的进程之间传递文件描述符呢?
在Linux下,可以利用UNIX域socket在进程间传递特殊的辅助数据,以实现文件描述符的传递