文章目录
第十三章 多进程编程
13.1 fork系统调用
- 概述
pid_t fork(void);
fork会复制当前进程,在内核进程表中创建一个新的进程表项;
新进程表项有很多与原进程相同的属性:比如堆指针、栈指针、标志寄存器;
新进程表项也有与原进程不同的属性:比如PPID、信号位图
(被清除)等
写时复制:父进程与子进程代码完全相同;子进程还会复制父进程的数据(堆、栈和静态数据),复制采用写时复制;
文件描述符:父进程中打开的文件描述符默认子子进程中也是打开的,且文件描述符引用计数+1
;父进程的用户根目录、当前根目录等变量的引用计数也会+1
13.2 exec系统调用
- 概述
path:可执行文件的完整路径;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* path, char* const argv[], char* const envp[]);
file:文件名,文件具体位置在环境变量PATH中搜寻;
arg:可变参数;
argv:参数数组;
envp:设置新程序的环境变量,默认是全局变量environ
exec系统调用通常是不返回的
,exec后的代码不会被执行……
exec不会关闭原程序打开的文件描述符……
13.3 处理僵尸进程
- 产生僵尸进程的条件
情况1:子进程结束,但是尚未被父进程回收;
情况2:父进程先结束,子进程被init进程(pid为1)接管,但是尚未被init进程回收 - wait/waitpid API
pid_t wait(int* stat_loc); pid_t waitpid(pid_t pid, int* stat_loc, int options);
- wait调用
wait系统调用将阻塞进程
,直到该进程的某个子进程结束运行为止;
子进程的结束状态会存储到stat_loc中;
返回值为子进程的PID - waitpid调用
waitpid只等待指定的子进程结束,且当option为WNOHANG时,waitpid是非阻塞的
;
如果pid取-1,则和wait一样等待任意一个子进程结束;
其他参数、返回值含义与wait相同 - waitpid的正确使用方式
要在事件已经发生的情况下执行非阻塞系统调用才有效率
;
=> 当SIGCHLD信号发送给父进程时,再调用waitpid,从而提高效率……// SIGCHLD信号的处理函数 static void handle_child(){ pid_t pid; int stat; while((pid=waitpid(-1,&stat,WNOHANG)) > 0){ // 对结束的子进程进行善后处 } }
13.4 管道
- 概述
匿名管道能在父子进程间传递数据,利用的是fork系统调用之后两个管道文件描述符fd[0]和fd[1]都保持打开 => 之后父子进程必须有一个关闭fd[0]一个关闭fd[1]
(匿名)管道只能单向传输,且只能在父子进程/有关联的进程间传递数据;
sockpair则是全双工的
;
命名管道FIFO能用于无关联的进程间通信(网络编程中使用不多)
13.5 信号量
- 概述
进程对共享资源访问的代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件
=> 这段关键代码就是临界区
;
部分通过软件方式实现的同步/互斥依赖于忙等待
,CPU利用率低;
信号量的实现不是简单的软件实现,因为不能保证检测变量和更新变量的原子性 =>信号量的实现必须依赖于关中断/test-and-set/CAS等硬件基础
;
下面讨论的是System V下的信号量!!! - 1.semget系统调用
semget系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集int semget(key_t key, int num_sems, int sem_flags);
key:信号量集的标志(类似于filename)
=> 不过它是一个整数,与返回的标志值不同;
num_sems:信号量集中的信号个数;
sem_flags:一组标志:其低9个bit是信号量的权限……;
返回值:信号量集的标志(类似于fd)
IPC_PRIVATE
:如果key为IPC_PRIVATE,则无论是否信号量都已经存在,都将创建一个新的信号量……更多可找man……
信号量集的数据结构struct semid_ds{ struct ipc_perm sem_perm; // 信号量的操作权限 unsigned long int sem_nsems; // 信号量数目 time_t sem_otime; // 最后一次调用semop的时间 time_t sem_ctime; // 最后一次调用semctl的时间 ..... }; struct ipc_perm{ key_t key; // 键值 uid_t uid; // 所有者的有效用户id gid_t gid; // 所有者的有效组id uid_t cuid; // 创建者的有效用户id gid_t cgid; // 创建者的有效组id mode_t mode; // 访问权限 ...... };
- 2.semop系统调用
sem_id:semget返回的信号量集标志;// 每个信号量关联的一些内核变量 unsigned short semval; // 信号量的值 unsigned short semzcnt; // 等待信号量值变为0的进程数 unsigned short semncnt; // 等待信号量值增加的进程数 pid_t sempid; // 最后一次进行semop操作的进程ID int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops); struct sembuf{ unsigned short int sem_num; // 要操作的信号量在信号量集中的编号 short sem_op; short sem_flg; };
sem_ops:信号量操作的结构体数组,每个成员指定对集合中哪个信号量的什么操作……;
num_sem_ops:sem_ops中元素个数;
semop是非阻塞的
,无论操作是否成功,调用都将立即返回 - 3.semctl系统调用
sem_id:semget返回的信号量集标志;int semctl(int sem_id, int sem_num, int command,...);
sem_num:要操作的信号量在信号量集中的编号;
command:具体的操作命令;
13.6 共享内存
-
概述
共享内存是最高效的IPC机制
,因为它不涉及进程之间的任何数据传输;
需要使用其他辅助手段来同步对共享内存的访问,否则会产生竞态条件;
因此,共享内存通常与其他进程间通信方式一起使用 => 比如信号量用于同步……
主要包括四个系统调用:shmget、shmat、shmdt、shmctl(System V下的方式) -
1.shmget系统调用
shmget用于创建一段新的共享内存,或者获取一段已经存在的共享内存;int shmget(key_t key, size_t size, int shmflg);
key:共享内存的标志(类似于filename)
=> 不过它是一个整数,与返回的标志值不同;
size:共享内存段的大小;
shmflg:……
返回值:共享内存标志符(类似于fd)
共享内存数据结构
如果shmgte用于创建共享内存,则这段共享内存的所有字节都被初始化为0
;
共享内存对应的数据结构shmid_ds:struct shmid_ds{ struct ipc_perm shm_perm; // 共享内存的操作权限 size_t shm_segsz; // 共享内存大小,单位是字节 __time_t shm_atime; // 对这段共享内存最后一次调用shmat的时间 __time_t shm_dtime; // 对这段共享内存最后一次调用shmdt的时间 __time_t shm_ctime; // 对这段共享内存最后一次调用shmctl的时间 };
-
2.shmat系统调用
shmat用于将共享内存块关联到进程的地址空间void* shmat(int shm_id, const void* shm_addr, int shmflg);
shm_id:shmget返回的共享内存标志符;
shm_addr:指定将共享内存关联到进程的哪块地址空间(最终效果受到shmflg影响)
shmflg:……
返回值:成功时返回共享内存被关联到的地址;失败时返回(void*)-1 -
3.shmdt系统调用
shmdt用于将关联到shm_addr的共享内存块从地址空间中分离int shmdt(const void* shm_addr);
返回值:成功时返回0;失败时返回-1
-
4.shmctl系统调用
int shmctl(int shm_id, int command, struct shmid_ds* buf);
-
共享内存的POSIX方法
前面介绍的系统调用时System V下的共享内存方式;
POSIX的共享内存方式有所区别,POSIX共享内存相关系统调用如下:// 创建共享内存,并返回一个文件描述符 int shm_open(const char* name, int oflag, mode_t mode); // 将文件描述符关联到进程地址空间 void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset); // 删除共享内存 int shm_unlink(const char* name);
POSIX下实现共享内存的方式为:
shm_open => mmap => shm_unlink
-
共享内存示例代码
9.6聊天室程序的改进,使用共享内存!!!必看
13-4shm_talk_server.cpp
13.7 消息队列
- 1.msgget系统调用
int msgget(key_t key, int msgflg); struct msgid_ds{ ...... };
- 2.msgsnd系统调用
msg_ptr:指向将要发送的消息数据;int msgsnd(int msgid,const void* msg_ptr,size_t msg_sz,int msgflg); struct msgbuf{ long mtype; // 消息类型 char mtext[512]; // 消息数据 };
msg_sz:消息的数据部分mtext的长度
msgflg:控制msgsnd的行为,通常仅支持IPC_NOWAIT,即以非阻塞的方式发送
;如果消息队列满了,则msgnd将阻塞 - 3.msgrcv系统调用
int msgrcv(int msgid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
- 4.msgctl系统调用
int msgctl(int msgid, int command, struct msgid_ds* buf);
13.8 IPC命令
- 概述
ipcs:显示当前系统上共享资源实例;
ipcrm:删除共享资源
13.9 在进程间传递文件描述符
- 概念
传递文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创建一个新的文件描述符
,而且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项
- 实现方式
父子进程:父进程打开描述符仍然在子进程中保持打开,从而自然传递……
不相关进程:利用UNIX域socket(sockpair)以及sendmsg、recvmsg在进程间传递特殊辅助数据
,以实现文件描述符的传递
示例代码:13-5passfd.cpp
注意:通过sendmsg发送的fd,并不是将fd值传递给目标进程,而是活生生地在目标进程空间里复制指向同一个file结构体的fd,所以不要期望在两个进程中,fd值相同(应该是sendmsg或者recvmsg内部对发送的fd进程了转换,先得到所有进程共享的文件表的索引,再转换成接收进程中打开文件表对应的文件描述符?
)
第十四章 多线程编程
14.1 Linux线程概述
- 线程模型
三种实现方式,参考:线程的三种实现方式?
- Linux线程库
Linux上默认使用线程库NPTL
,比较符合POSIX标准,且已经成为glibc的一部分
;
详见P270……
14.2 创建线程和结束线程
- 1.pthread_create
thread:新线程的标志符(pthread_t是整型);int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*),void* arg);
attr:设置新线程的属性,NULL表示使用默认属性;
start_routine:指定新线程将运行的函数;
arg:新线程将运行的函数的参数 - 2.pthread_exit
通过retval参数向线程的回收者传递退出信息;void pthread_exit(void* retval);
它执行之后不会返回
,且永远不会失败! - 3.pthread_join
一个进程中的所有线程都可以调用pthread_join函数来回收其他线程
,即等待其他线程结束;
这类似于回收进程的wait和waitpid
;
thread:目标线程的标志符;int pthread(pthread_t thread,void** reteval);
retval:目标线程返回的退出信息;
pthread_join会一直阻塞,直到被回收的线程结束为止
- 4.pthread_cancel
pthread_cancel用于异常终止一个线程,即取消线程;
类似于Java的interrupt(),是否取消以及如何取消由目标线程决定
thread:目标线程标志符;int pthread_cancel(pthread_t thread); // 这两个函数被目标线程调用... int pthread_setcancelstate(int state, int* oldstate); int pthread_setcanceltype(int type, int* oldtype);
14.3 线程属性
- 线程属性结构体
#define __SIZEOF_PTHREAD_ATTR_T 36 typedef union{ char __size[__SIZEOF_PTHREAD_ATTR_T]; long int __align; }pthread_attr_t;
- 线程属性操作函数
- 线程属性含义
detachstate:线程脱离状态。脱离线程在退出时将自行释放其占用的系统资源;
stackaddr和stacksize:线程栈的起始地址和大小;
guardszie:保护区域大小,在堆栈尾部分配,用于保护堆栈不被错误覆盖;
schedparam:线程调度参数;
scope
:线程间竞争CPU的范围,即线程优先级的有效范围(即仅统一进程内竞争,还是所有线程一起竞争)
14.4 POSIX信号量(线程同步)
- 概述
Linux下,有两组信号量API:System V
(13章)、POSIX
(本章);前者使用信号量集,后者使用单个的信号量;
三种专门用于线程同步的机制
:POSIX信号量、互斥量、条件变量 - POSIX信号量 API
int sem_init(sem_t* sem, int pshared, unsigned int value); // shared指明信号量是否在进程间共享 int sem_destroy(sem_t* sem); int sem_wait(sem_t* sem); // 阻塞 => P操作 int sem_trywait(sem_t* sem); // 非阻塞 int sem_post(sem_t* sem); // => V操作
14.5 互斥锁(线程同步)
- 概述
用于保护关键代码,确保其独占式访问;
类似于二进制信号量
(Linux下是通过信号量实现的吗?); - 互斥锁基础API
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr); int pthread_mutex_destroy(pthread_mutex_t* mutex); int pthread_mutex_lock(pthread_mutex_t* mutex); // P操作,阻塞 int pthread_mutex_trylock(pthread_mutex_t* mutex); // 非阻塞 int pthread_mutex_unlock(pthread_mutex_t* mutex); // V操作
- 互斥锁属性
略…… - 死锁举例
14-1mutual_lock.c
14.6 条件变量(线程同步)
- 概述
互斥锁:用于同步线程对共享数据的访问;
条件变量:用于在线程之间同步共享数据的值 =>当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程
- 条件变量API
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr); int pthread_cond_destroy(pthread_cond_t* cond); int pthread_cond_broadcast(pthread_cond_t* cond); int pthread_cond_signal(pthread_cond_t* cond); int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
pthread_cond_broadcast
:以广播的方式唤醒所有等待目标条件变量的线程;
pthread_cond_signal
:唤醒一个等待目标条件变量的线程;
pthread_cond_wait
:用于等待目标条件变量
14.7 线程同步机制包装类
- 概述
这里包装只是为了后续章节使用方便,参考:14-2locker.h
14.8 多线程环境
-
可重入函数
如果一个函数能被多个线程同时调用且不发生静态条件,则称它为线程安全的,或者说它是可重入函数
(严格来说线程安全和可重入稍有区别……)。部分库函数之所以不可重入,主要是因为其内部使用了静态变量
-
线程和进程
如果一个多线程程序的某个线程调用了fork函数,子进程仍然只拥有一个线程,它是调用fork的那个线程的完整复制
;
不过子进程将自动继承父进程中互斥锁、条件变量等的状态
=> 即父进程中已被加锁互斥锁在子进程中仍然是被锁住的;
所以,若子进程再对该互斥锁执行加锁操作,将导致死锁,示例代码:14-3thread_atfork.c
确保父子进程清楚锁状态int pthread_atfork(void (*prepare)(void), void(*parent)(void),void(*child)(void));
prepare:在fork调用创建出子进程之前执行,用于锁住父进程中的互斥锁;
parent:fork调用创建出子进程之后,返回之前,在父进程中执行,用于释放所有在prepare中锁住的互斥锁;
child:fork返回之前,在子进程中执行,也是用于释放所有在prepare中被锁住的互斥锁
感觉这里没总结清除,详见P284…… -
线程和信号
多线程中应该使用线程掩码而不是进程掩码
(每个线程有自己的信号掩码)int pthread_sigmask(int how, const sigset_t* newmask, sigset_t* oldmask);
1.
进程中所有线程共享该进程的信号
,线程库将根据线程掩码决定把信号发送给哪个具体的线程;
2.所有线程共享信号处理函数
……
=> 综上,应该使用一个专门的线程来处理所有信号,代码示例:14-5sigmask.c
将信号发送给指定线程int pthread_kill(pthread_t thread, int sig);
第十五章进程池和线程池
15.1 进程池和线程池概述
- 概述
1. 本章以进程池为例,不过后面的讨论也适用于线程池;
2.进程池
是由服务器预先创建的一组子进程,子进程的数目通常在3~10个之间;
3. 进程池中素有子进程都运行着相同的代码,具有相同的属性,比如优先级、PGID等;
4.由于启动之初就创建,所以每个子进程相对干净,即他们没有打开不必要的文件描述符(从父进程继承而来),也不会错误地使用大块的堆内存(从父进程复制得到)
;
5. 主进程选择好执行任务的子进程后,主进程需要通知目标子进程并传递数据 => 最简单的方式是通过管道……
6. 进程池模型:
主进程选择执行任务的子进程的方法可以是:随机算法、Round Robin、工作队列
等