多进程
创建进程
当使用多进程模式执行程序时,分叉之前需要创建一个进程
#include <unistd.h>
//创建一个进程,返回该进程在程序中的编号
pid_t fork(void);
//返回进程编号
pid_t getpid(void);
注意:
- 子进程和父进程共享代码段,但是会复制数据段
- 父进程中fork函数返回子进程的编号,子进程中fork函数返回0
- 若子进程优先于父进程结束,那么子进程会成为僵尸进程,这里需要设置信号处理函数去避免
- 若父进程优先于子进程结束,那么子进程会成为孤儿进程,将被系统托管,在后台运行
- 子进程与父进程之间的数据是相互独立的,尽管使用指针,也并不能影响其他的进程
避免生成僵尸进程的方法:
#include <signal.h>
//忽略子进程的退出信号,避免产生僵尸进程
signal(SIGCHLD,SIG_INT);
由于fork函数创建的子进程会完全复制父进程,但我们使用时并不会希望是这种效果,所以需要使用exec族函数进行进程的替换:
// 函数后的注释是按照 参数格式、是否带路径、是否使用当前环境变量的顺序组成的
int execl(const char * path,const char * arg, …);// list 不带 是
int execlp(const char * file,const char * arg, …);// list 带 是
int execle(const char * path,const char * arg, … char const *envp[]);// list 不带 需自己组装环境变量
int execv(const char * path,char* const argv[]);// array 不带 是
int execvp(const char * file,char* const argv[]);// array 带 是
int execve(const char * path,char* const argv[], char const *envp[]);// array 不带 需自己组装环境变量
上边也已经提到了,子进程和父进程是相互独立的,尽管使用指针也不能相互影响,所以多个进程之间的通讯就需要特定的函数来完成。
进程间通讯
多个进程之间通讯,主要方式有 管道、信号量、消息队列、信号、共享内存、内存映射和socket几种形式。
管道
管道分为命名管道和无名管道,对于命名管道FIFO来说,IO操作和普通管道IO操作基本一样,但是两者有一个主要的区别,在命名管道中,管道可以是事先已经创建好的,比如我们在命令行下执行
mkfifo myfifo
就是创建一个命名通道,我们必须用open函数来显示地建立连接到管道的通道,而在管道中,管道已经在主进程里创建好了,然后在fork时直接复制相关数据或者是用exec创建的新进程时把管道的文件描述符当参数传递进去。
一般来说FIFO和PIPE一样总是处于阻塞状态。也就是说如果命名管道FIFO打开时设置了读权限,则读进程将一直阻塞,一直到其他进程打开该FIFO并向管道写入数据。这个阻塞动作反过来也是成立的。如果不希望命名管道操作的时候发生阻塞,可以在open的时候使用O_NONBLOCK标志,以关闭默认的阻塞操作。
无名管道
#include <unistd.h>
// 成功 返回0,失败返回-1。
int pipe(int pipe_interface[2]);// 建立管道,该函数在数组上填上两个新的文件描述符
// 列如
int fd[2]
int result = pipe(fd);
通过使用底层的read和write调用来访问数据。 向 pipe_interface[1]写 数据,从 pipe_interface[0]中 读数据。写入与读取的顺序原则是 先进先出。
管道读写规则
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
当要写入的数据量不大于PIPE_BUF(Posix.1要求PIPE_BUF至少 512字节)时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
命名管道
#include <sys/types.h>
#include <sys/stat.h>
// 成功则返回0,否则返回-1,错误原因存于errno中。
int mkfifo(const char *fileName, mode_t mode); // 建立一个名字为fileName的命名管道,mode为该文件的权限(mode%~umask)
// 例如
mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
命名管道是一种特殊的文件,存在于系统中,所以它可以允许没有亲缘关系的两个进程进行通讯
信号量
信号量是一个计数器,常被当做锁去使用。信号量的值通常为一个整数值,通常使用wait和signal操作来改变信号量的值,也被称为PV操作
p:如果信号量的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行
V:如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1
//获取或创建信号量
int semget(key_t key, int nsems, int semflg);
参数key是信号量的键值,typedef unsigned int key_t,是信号量在系统中的编号,不同信号量的编号不能相同,这一点由程序员保证
key用十六进制表示比较好。
参数nsems是创建信号量集中信号量的个数,该参数只在创建信号量集时有效,这里固定填1。
参数sem_flags是一组标志,如果希望信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。如果没有设置IPC_CREAT标志并且信号量不存在,就会返错误(errno的值为2,No such file or directory)。
如果semget函数成功,返回信号量集的标识;失败返回-1,错误原因存于error中。
//控制信号量(常用于设置信号量的初始值和销毁信号量
int semctl(int semid, int sem_num, int command, ...);
参数semid是由semget函数返回的信号量标识。
参数sem_num是信号量集数组上的下标,表示某一个信号量,填0。
参数cmd是对信号量操作的命令种类,常用的有以下两个:
IPC_RMID:销毁信号量,不需要第四个参数;
SETVAL:初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。第四参数是一个自定义的共同体,如下:
如果semctl函数调用失败返回-1
// 用于信号灯操作的共同体。
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
//等待信号量的值变为1,如果等待成功,立即把信号量的值置为0,这个过程也称之为等待锁;
//把信号量的值置为1,这个过程也称之为释放锁。
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数semid是由semget函数返回的信号量标识。
参数sops是一个结构体,如下:
参数nsops是操作信号量的个数,即sops结构变量的个数,设置它的为1(只对一个信号量的操作)
struct sembuf
{
short sem_num; // 信号量集的个数,单个信号量设置为0。
short sem_op; // 信号量在本次操作中需要改变的数据:-1-等待操作;1-发送操作。
short sem_flg; // 把此标志设置为SEM_UNDO,操作系统将跟踪这个信号量。
// 如果当前进程退出时没有释放信号量,操作系统将释放信号量,避免资源被死锁。
};
信号
信号是基于UNIX系统的信号机制,用于一个或几个进程之间传递异步信号。信号可以有各种异步事件产生,比如键盘中断等。shell也可以使用信号将作业控制命令传递给它的子进程。
#include <sys/types.h>
#include <signal.h>
// 设置信号拦截处理函数
void (*signal(int ,void (*func)(int)))(int);
sig:需要拦截处理的信号
func:处理信号用到的函数指针
返回值:返回该信号先前的处理函数的指针
// 进程号为pid的进程发送信号
int kill(pid_t pid,int sig);//信号值为sig。当pid为0时,向当前系统的所有进程发送信号sig。
int raise(int sig);// 向当前进程发送信号。
共享内存
共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在该进程的地址空间(这里的地址空间具体是哪个地方?)中。其他进程可以将 同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。
共享内存是 IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。
注意:共享内存本身并没有同步机制,需要程序员自己控制。
#include <sys/ipc.h>
#include <sys/shm.h>
//创建共享内存
int shmget(key_t key, size_t size, int shmflg);
key:共享内存的键值,是一个整数,typedef unsigned int key_t,是共享内存在系统中的编号,不同共享内存的编号不能相同,这一点由程序员保证。
key用十六进制表示比较好。
size:待创建的共享内存的大小,以字节为单位。
shmflg:共享内存的访问权限,与文件的权限一样,0666|IPC_CREAT表示全部用户对它可读写,如果共享内存不存在,就创建一个共享内存。
返回共享内存的标识符
//将共享内存连接到当前进程
void *shmat(int shm_id, const void *shm_addr, int shmflg);
参数shm_id是由shmget函数返回的共享内存标识。
参数shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
参数shm_flg是一组标志位,通常为0。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
//将共享内存从当前进程中剥离
int shmdt(const void *shmaddr);
参数shmaddr是shmat函数返回的地址。
调用成功时返回0,失败时返回-1.
//删除共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);
参数shm_id是shmget函数返回的共享内存标识符。
参数command填IPC_RMID。
参数buf填0。
注:
1、共享内存创建后不会随着程序的结束而释放,而是会在全系统的各个进程之间通用,
2、共享内存中的内容不会自动清空,再改变之前,一直保存着上一次写入的值
3、共享内存不存在锁,多个程序同时对共享内存进行操作时,可能会出现冲突
4、用信号量可以充当共享内存的锁
5、查看当前系统的共享内存:ipcs -m
6、删除某个共享内存:ipcrm -m 共享内存编号
内存映射
内存映射文件,是由一个文件到一块内存的映射。内存映射文件与 虚拟内存有些类似,通过内存映射文件可以保留一个地址的区域,
同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作。 每一个使用该机制的进程通过把同一个共享的文件映射到自己的进程地址空间来实现多个进程间的通信(这里类似于共享内存,只要有一个进程对这块映射文件的内存进行操作,其他进程也能够马上看到)。
使用内存映射文件不仅可以实现多个进程间的通信,还可以用于 处理大文件提高效率。因为我们普通的做法是 把磁盘上的文件先拷贝到内核空间的一个缓冲区再拷贝到用户空间(内存),用户修改后再将这些数据拷贝到缓冲区再拷贝到磁盘文件,一共四次拷贝。如果文件数据量很大,拷贝的开销是非常大的。那么问题来了,系统在在进行内存映射文件就不需要数据拷贝?mmap()确实没有进行数据拷贝,真正的拷贝是在在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,所以只进行一次数据拷贝。效率高于read/write。
#include <sys.mman.h>
void *mmap(void*start,size_t length,int prot,int flags,int fd,off_t offset); //mmap函数将一个文件或者其它对象映射进内存。 第一个参数为映射区的开始地址,设置为0表示由系统决定映射区的起始地址,第二个参数为映射的长度,第三个参数为期望的内存保护标志,第四个参数是指定映射对象的类型,第五个参数为文件描述符(指明要映射的文件),第六个参数是被映射对象内容的起点。成功返回被映射区的指针,失败返回MAP_FAILED[其值为(void *)-1]。
int munmap(void* start,size_t length); //munmap函数用来取消参数start所指的映射内存起始地址,参数length则是欲取消的内存大小。如果解除映射成功则返回0,否则返回-1,错误原因存于errno中错误代码EINVAL。
int msync(void *addr,size_t len,int flags); //msync函数实现磁盘文件内容和共享内存取内容一致,即同步。第一个参数为文件映射到进程空间的地址,第二个参数为映射空间的大小,第三个参数为刷新的参数设置。
多线程
线程的创建、等待、退出、清理
#include <pthread.h>
//创建一个线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数thread为为指向线程标识符的地址。
参数attr用于设置线程属性,一般为空,表示使用默认属性。
参数start_routine是线程运行函数的地址,填函数名就可以了。
参数arg是线程运行函数的参数。新创建的线程从start_routine函数的地址开始运行,该函数只有一个无类型指针参数arg。
若成功,返回0;若出错,返回出错编号。
注:若要想向start_routine传递多个参数,可以将多个参数放在一个结构体中,然后把结构体的地址作为arg参数传入,但是要非常慎重,程序员一般不会这么做。
#include <pthread.h>
//线程的正常退出,类似于return
void pthread_exit(void *retval);
参数retval一般填0
注:在线程中调用exit函数会退出整个进程
线程的终止有三种方式:
1)线程的start_routine函数代码结束,自然消亡。
2)线程的start_routine函数调用pthread_exit结束。
3)被主进程或其它线程中止。
子线程退出后,其资源不会自动释放,产生僵尸线程
#include <pthread.h>
//阻塞进程,等待子进程的返回,并回收子进程的资源
int pthread_join(pthread_t thread, void **value_ptr);
thread:等待退出线程的线程号。
value_ptr:保存线程函数退出时的返回值。
返回值:0代表成功。 失败,返回的则是错误号,可以用来判断线程退出的状态
#include <pthread.h>
//设置线程的属性为PTHREAD_CREATE_JOINABLE,既退出时会自动释放资源
int pthread_detach(pthread_t tid);
tid:线程标识符
返回值:成功返回零。其他任何返回值都表示出现了错误
线程退出后,有极大的可能会产生僵尸进程,下班列举几个解决方法
//解决僵尸进程的四种方法
1)方法一:创建线程后,在创建线程的程序中调用pthread_join等待线程退出,一般不会采用这种方法,因为pthread_join会发生阻塞。
pthread_join(pthid,NULL);
2)方法二:创建线程前,调用pthread_attr_setdetachstate将线程设为detached,这样线程退出时,系统自动回收线程资源。
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); // 设置线程的属性。
pthread_create(&pthid,&attr,pth_main,(void*)((long)TcpServer.m_clientfd);
3)方法三:创建线程后,在创建线程的程序中调用pthread_detach将新创建的线程设置为detached状态。
pthread_detach(pthid);
4)方法四:在线程主函数中调用pthread_detach改变自己的状态。
pthread_detach(pthread_self());
线程退出的善后函数一般不会写在线程的主函数中,因为线程不一定会在某个确切的地方退出,所以就需要提前注册线程的清理函数
#include <pthread.h>
//注册线程的清理函数
void pthread_cleanup_push(void(*routine)(void*),void,argv);
routine:注册的清理函数名,返回值和参数均为void*类型
argv:传给清理函数的参数
// 弹出或弹出并执行一个清理函数
void pthread_cleanup_pop(int execute);
execute:为0时只会弹出一个清理函数但不执行,非0时弹出并执行一个清理函数
注:
1、这两个函数必须成对出现在同一个语句块中,否则会报错
2、清理函数的注册列表类似于栈,是后注册的函数先弹出
3、当线程通过调用pthread_exit()函数终止时,会执行所有的清理函数
4、当进程通过调用return函数终止时,则不会调用清理函数(但实际上会执行?)
线程同步
线程同步一般使用的是互斥锁、条件变量、信号量、自旋锁、读写锁等
互斥锁
互斥锁顾名思义,同一时间只能由一个进程通过。实际操作中,互斥锁会出现优先唤醒的问题:总是会倾向于一个线程连续多次对同一个互斥锁上锁。
#include <pthread.h>
pthread_mutex_t mutex;//声明创建一个互斥锁
//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);
其中参数 mutexattr 用于指定锁的属性(见下),如果为NULL则使用缺省属性。
互斥锁的属性在创建锁的时候指定,当资源被某线程锁住的时候,其它的线程在试图加锁时表现将不同。当前有四个值可供选择:
1)PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
2)PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。
3)PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。
4)PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,等待解锁后重新竞争。
//阻塞加锁
int pthread_mutex_lock(pthread_mutex *mutex);
如果是锁是空闲状态,本线程将获得这个锁,并把他加锁
如果锁已经被占据,本线程将排队等待,直到成功的获取锁。
//非阻塞加锁
int pthread_mutex_trylock( pthread_mutex_t *mutex);
该函数语义与 pthread_mutex_lock() 类似,
不同的是在锁已经被占据时立即返回 EBUSY,不是挂起等待。
//解锁
int pthread_mutex_unlock(pthread_mutex *mutex);
线程把自己持有的锁释放
//销毁锁
int pthread_mutex_destroy(pthread_mutex *mutex);
销毁锁之前,锁必需是空闲状态(unlock)
若销毁时不是空闲状态,则返回EBUSY
条件变量
与互斥锁不同,条件变量可以对同时多个进程放行。使用条件变量的线程,同时等待某一个条件变量。当条件变量放行时,所有阻塞于该变量的进程全部进入运行态
#include <pthread.h>
pthread_cond_t cond;//声明一个条件变量
//初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
cond:条件变量的代号
attr:条件变量属性,一般填NULL表示默认值
//销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//永久阻塞等待条件变量满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
cond:需要等待的条件变量
mutex:利用的互斥锁
在这个函数中,共进行三个步骤:
1、首先释放互斥锁mutex
2、开始阻塞,等待条件变量满足
3、当条件变量满足时,给互斥锁mutex加锁,解除阻塞
注:使用pthread_cond_wait()之后,若不再调用,需要手动释放mutex,否则等待队列
中的其他线程将不会被环形
//限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
cond:需要等待的条件变量
mutex:利用的互斥锁
abstime:等待结束的绝对时间,结构体定义如下
struct timespec {
time_t tv_sec; //秒
long tv_nsec; //纳秒
}
注:时间结构体的正常使用方法如下:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t) (现时1秒等待条件变量)
//唤醒一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
此函数会唤醒条件变量等待队列上的第一个线程
//唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
信号量
信号量使用可以限制同时启动的线程的个数。通过设置信号量的初始值,只要信号量不为0,就可以放行一个线程,并将信号量的值-1
#include <semaphore.h>
sem_t sem;//声明一个信号量
//创建信号量
int sem_init(sem_t *sem, int pshared, unsigned int value)
sem:信号量名
pshared:若等于0,则用于同一多线程的同步
若大于0,则用于多个相关进程的同步(即fork产生的)
value:信号量的初始值
//摧毁一个信号量
int sem_destroy(sem_t *sem);
//等待信号量
int sem_wait(sem_t *sem);//一直等待
int sem_trywait(sem_t *sem);//尝试访问,若不能则不阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout)//若超时则不等待
注:这三个函数会将信号量的值-1,若小于等于0,则会阻塞等待信号量变为大于0
//释放信号量
int sem_post(sem_t *sem);
注:这个函数会将信号量的值+1,若信号量>0, 则可以被获取
//获取信号量的值
int sem_getvalue(sem_t *restrict, int *val);
restrict:信号量名
val:存放信号量值的地址
自旋锁
自旋锁和互斥锁的用法基本相同
只是在处理机制上有所不同:
互斥锁等待时,会令申请者休眠,进而阻塞
自旋锁等待时,会一直循环判断是否满足条件,浪费CPU资源,同时也能更快的获得锁
#include <pthread.h>
pthread_spinlock_t mutex;//声明创建一个自旋锁
//初始化自旋锁
int pthread_spin_init(pthread_spinlock_t *spin, int pshare);
spin:自旋锁名
pshare:共享属性:
PTHREAD_PROCESS_SHARED:可被其他进程共享
PTHREAD_PROCESS_PRIVATE:只能在本线程中使用
//阻塞加锁
int pthread_spin_lock(pthread_spinlock_t *mutex);
如果是锁是空闲状态,本线程将获得这个锁,并把他加锁
如果锁已经被占据,本线程将排队等待,直到成功的获取锁。
//非阻塞加锁
int pthread_mutex_trylock( pthread_spinlock_t *spin);
该函数语义与 pthread_mutex_lock() 类似,
不同的是在锁已经被占据时立即返回 EBUSY,不是挂起等待。
//解锁
int pthread_mutex_unlock(pthread_spinlock_t *spin);
线程把自己持有的锁释放
//销毁锁
int pthread_mutex_destroy(pthread_spinlock_t *spin);
销毁锁之前,锁必需是空闲状态(unlock)
若销毁时不是空闲状态,则返回EBUSY
读写锁
读写锁可以同时放行一个写进程或多个读进程
1、如果当前锁已经被读锁,其他线程可以再申请读锁,但不能申请写锁
2、如果当前线程已被写锁,则其他线程即不能申请读锁,也不能申请写锁
读写锁没有硬性规定持有读锁的时候不能写,持有写锁的时候不能读,应该由程序自己区保持读写锁的属性
#include <pthread.h>
pthread_rwlock_t rwlock;//声明一个读写锁
//初始化读写锁
int pthread_rwclock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
//摧毁读写锁
int pthread_rwclock_destory(pthread_rwlock_t *rwptr);
//阻塞线程加读出锁,如果读写锁被一个写入者持有则堵塞
int pthread_rwclock_rdclock(pthread_rwlock_t *rwptr);
//阻塞线程加写入锁,如果读写锁被读或者写线程占有,则堵塞等待
int pthread_rwclock_wrclock(pthread_rwlock_t *rwptr);
// 非阻塞模式,如果不能马上得到就返回一个EBUSY错误
int pthread_rwclock_tryrdclock(pthread_rwlock_t *rwptr);
int pthread_rwclock_trywrclock(pthread_rwlock_t *rwptr);
//释放锁
int pthread_rwclock_unclock(pthread_rwlock_t *rwptr);
//设置读写锁的属性
int pthread_rwclock_setpshared(const pthread_rwlockattr_t *attr, int valptr);
第二个参数表示需要设置的属性,可选值如下
PTHREAD_PROCESS_PRIVATE:只在本进程可用
PTHREAD_PROCESS_SHARED:可以多进程共享