多进程
1、进程的创建fork()
#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);
返回值: > 0 父进程 parent process
==0 子进程 child process
< 0(-1) 失败
fork的特点:
第一个特点: 一次调用,两次返回,返回大于0(值就是子进程的id号)表示父进程,等于0表示子进程
子进程会复制父进程的所有资源(代码段,数据段)
第二个特点:父进程总共分成三个部分
第一个部分在fork之前
第二个部分fork成功之后,在id>0情况下的那部分代码
第三个部分fork后面的
总结:
第一点:fork()的套路
if(>0){ 父进程代码 }else if(==0){ 程序员创建子进程需要并发执行的任务代码 }else{ 错误 } 第二点:vfork创建的子进程共享父进程的资源
第二点:vfork创建的子进程一定优先于父进程运行
2、进程的退出跟回收exit()/wait()
#include <stdlib.h>
void exit(int status); //退出进程的同时刷新缓冲区
void _exit(int status);//退出进程的时候不刷新缓冲区
参数:status -->进程退出时候的值
对比return: 区别一:return是C语言中关键字,exit()是函数
区别二:return是返回函数调用的结果,exit()是结束整个进程
#include <sys/wait.h>
pid_t wait(int *stat_loc);--->收子进程结束返回值(收尸)
返回值:成功 返回值回收到的那个子进程的ID,失败 -1
参数:stat_loc -->用来存放进程退出时候的状态信息
不是存放退出值,退出值仅仅只是状态信息中的一部分
特点:会让父进程一直阻塞,直到成功回收到子进程为止
pid_t waitpid(pid_t pid, int *stat_loc, int options);
返回值:成功 返回值回收到的那个子进程的ID,失败 -1
参数: pid --> 小于-1 waitpid(-1200,&status,options);回收进程组id是1200的其中一个
等于-1 waitpid(-1,&status,options);回收任意一个子进程(不要求同组)
等于0 waitpid(0,&status,options); 回收本进程组中任意一个子进程
大于0 waitpid(1200,&status,options); 指定回收id是1200的子进程
stat_loc -->跟wait一样
options -->WN0HANG 非阻塞等待,等得到就等,等不到就直接退出
0 阻塞等待
3、获取当前进程的id以及父进程的id
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //获取子进程ID
pid_t getppid(void); //获取父进程ID
gid_t getgid(void); //获取进程组ID
4、使用函数执行shell命令
#include <stdlib.h>
#include <unistd.h>
int system(const char *command);
返回值:失败 -1
参数: command -->你要执行的shell命令或者你要执行程序完整的命令名
int execl(const char *path, const char *arg, ...);
参数: path -->你要执行的程序/命令所在的路径
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 execvpe(const char *file, char *const argv[],char *const envp[]);
参数: file-->即将被加载执行的文件名
argv-->保存即将被执行的参数的数组
envp-->用户自定义的环境变量数组
总结: l -->参数以列表的形式逐一列举
e -->参数中可以设置环境变量
v -->参数用一个指针数组存放
p -->传递程序/命令的名字
进程间通信
无名管道和有名管道
1、创建无名管道
#include <unistd.h>
int pipe(int fildes[2]);
返回值:成功 0 失败 -1
参数: fildes[2] -->存放的是两个文件描述符
fildes[0]读端的文件描述符
fildes[1]写端的文件描述符
特点:
有固定的读端(fd[0])跟写端(fd[1])
当管道中没有数据可以读的时候,调用read会阻塞
只能用于具有血缘关系的进程之间(父子进程,兄弟进程之间)
2、创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回值:成功 0 失败 -1
参数: pathname -->你要创建的有名管道的名字(带路径)
mode -->0777
特点:
有名字,生成一个管道文件,用于通信
当管道中没有数据可以读的时候,调用read会阻塞
任意两个不同进程之间都能通信
3、判断文件是否存在
#include <unistd.h>
int access(const char *path, int amode);
功能:判断一个文件是否存在,参数用来判断文件是否可读、是否可写、是否可执行
返回值:符合你要求的判断条件返回0 否则 -1
参数: path -->文件的路径
amode -->R_OK, W_OK, X_OK ,F_OK
信号:作用是当一个进程发送信号给另外一个进程的时候,可以通过该信号去控制另外一个进程执行程序员想干的事情
1、发送信号
#include <signal.h>
int kill(pid_t pid, int sig);
参数: pid -->你要发送信号的那个进程的id
sig -->你要发送的信号
int sigqueue(pid_t pid, int sig, const union sigval value);
参数:union sigval {
int sival_int;
void *sival_ptr; //void *万能指针
};//存放你想发送的额外数据
跟kill的区别:sigqueue买一送一(发送信号的同时可以额外发送其他数据给到进程)
2、捕捉信号并改变信号的响应动作
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
返回值:最近一次调用时第二个参数的值(函数指针)
参数:sig -->你想要捕捉的信号
第二个参数有三种情况:
情况一:
void (*func)(int) -->函数指针,你想要改变的信号动作就靠该函数实现
情况二:
SIG_DFL -->按照信号默认的动作响应
情况三:
SIG_IGN -->忽略信号,收到信号之后不做任何响应,直接舍弃
注意:在所有的信号中有两个信号是既不能改变默认动作也不能忽略,SIGKILL和SIGSTOP
int sigaction(int sig, const struct sigaction *restrict act,struct sigaction *restrict oact);
参数: struct sigaction
{
void(*)(int) sa_handler //跟signal中函数指针一模一样
sigset_t sa_mask //信号阻塞掩码??
int sa_flags //设置0选择sa_handler
//设置SA_SIGINFO表示选择sa_sigaction
void(*)(int,siginfo_t *,void *) sa_sigaction//另外一个信号响应函数,接收额外数据
}
void(*)(int,siginfo_t *,void *)
siginfo_t
{
si_int -->存放union sigval里面的sival_int
si_ptr -->存放union sigval里面的sival_ptr
si_pid -->存放发送信号的那个进程的id
}
oact-->原有信号的处理参数,一般为NULL
3、其它简单函数
#include <signal.h>
int pause(void);//阻塞当前进程等待信号到来
int raise(int sig);//自己给自己发送信号
unsigned alarm(unsigned seconds);//定时器,alarm(5);过5秒之后自己给自己发送SIGALRM
4、信号的阻塞或屏蔽
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set,sigset_t *restrict oset);//设置信号阻塞的函数
返回值:成功 0 失败 -1
参数:how -->SIG_BLOCK //设置阻塞 将set对应信号添加到原本的信号集合中
SIG_SETMASK //设置阻塞 用set替换原本的信号集合中的信号
SIG_UNBLOCK //解除阻塞
sigset_t --> 系统定义的一种变量类型,专门用来存放你想要阻塞的信号
称之为信号阻塞掩码集
操作信号阻塞掩码集合的函数
int sigemptyset(sigset_t *set); //清空掩码集
int sigfillset(sigset_t *set); //将所有的linux信号添加到集合中
int sigaddset(sigset_t *set, int signum);//将具体的某个信号添加到集合
int sigdelset(sigset_t *set, int signum);//将具体的某个信号从集合删除
int sigismember(const sigset_t *set, int signum);//判断某个信号在不在集合 返回1是成员 返回0不是成员
注意:信号设置阻塞仅仅只是将信号暂时挂起,信号依然存在(等到解除阻塞又能重新响应)
system-V IPC通信 :指的就是共享内存,消息队列,信号量
linux命令: ipcs -s 查看当前系统所有的信号量
ipcs -m 查看当前系统所有的共享内存
ipcs -q 查看当前系统所有的消息队列
ipcs -a 查看当前系统所有的IPC对象
ipcrm -s 信号量id 删除信号量
ipcrm -m 共享内存id 删除共享内存
ipcrm -q 消息队列id 删除消息队列
信号量:用来协调多个进程对应共享资源的访问
特点:当信号量的值为0,你还想p操作,会阻塞当前进程
信号量的值是不可能为负数的
v操作永远不会阻塞
1、创建信号量
#include <sys/sem.h>
#include <sys/ipc.h>
int semget(key_t key, int nsems, int semflg);
返回值:成功 信号量的id 失败 -1
参数:key -->键值,确保唯一性
产生键值两种方法
第一种:自己随便写一个(正整数)
第二种:使用系统提供的ftok()生成一个键值
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
返回值:成功 返回键值 失败 -1
参数:pathname -->合法的linux路径
proj_id -->随便写个整数
ftok(".",200); ftok(".",199);
nsems -->你打算创建多少个信号量
semflg -->IPC_CREAT信号量不存在则创建
IPC_EXCL信号量已存在则报错
0777 信号量的访问权限
2、获取或设置信号量的相关属性
#include <sys/sem.h>
#include <sys/ipc.h>
int semctl(int semid, int semnum, int cmd, ...);
参数: semid -->信号量的ID,semget的返回值
semnum -->信号量的序号,从0开始
cmd -->GETVAL //获取信号量值
int value=semctl(id,0,GETVAL);返回值给value
SETVAL //设置信号量值
semctl(id,0,SETVAL,10);//将第一个信号量值设置为10
IPC_RMID //删除信号量
3、信号量的PV操作
#include <sys/sem.h>
#include <sys/ipc.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
返回值:
参数:struct sembuf
{
short sem_num 信号量的序号
short sem_op 决定你究竟是想P操作还是V操作,负数P操作,正数V操作
short sem_flg SEM_UNDO(操作完信号量之后,恢复成原本值)
}
nsops -->信号量struct sembuf个数
共享内存:效率最高的IPC
1、申请共享内存
#include <sys/shm.h>
#include <sys/ipc.h>
int shmget(key_t key, size_t size, int shmflg);
返回值:成功 共享内存的ID 失败 -1
参数:size -->你打算申请多少的内存,字节,一般设置成512的整数倍
shmflg->IPC_CREAT共享内存不存在则创建
IPC_EXCL共享内存已存在则报错
0777 共享内存的访问权限
2、对共享内存进行映射或解除映射
#include <sys/shm.h>
#include <sys/ipc.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
参数:shmid-->共享内存id
shmaddr -->一般为NULL
shmflg -->一般为0
返回值:成功返回共享内存的首地址
失败 -1
3、获取、设置共享内存相关属性或删除共享内存
#include <sys/shm.h>
#include <sys/ipc.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:cmd -->IPC_STAT //获取共享内存的属性信息
IPC_SET //设置共享内存的属性信息
IPC_RMID //删除共享内存
struct shmid_ds -->用来存放共享内存的属性信息
消息队列
1、创建消息队列
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:shmflg->IPC_CREAT消息队列不存在则创建
IPC_EXCL消息队列已存在则报错
0777 消息队列的访问权限
2、收发信息
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
返回值:成功 0 失败 -1
参数:msgflg -->默认设置0 //阻塞
IPC_NOWAIT //非阻塞
msgp--->要发送数据的存储区域指针
msgsz-->要发送数据的大小
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
参数:msgtyp -->你要接收的信息类型
msgp--->要接收数据的存储区域指针
msgsz-->要接收数据的大小
重点:发送消息的格式是有要求的,要求用户自定数据类型
struct msgbuf
{
long msgtype; //表明消息类型
char truemsg[50];//真实的信息内容
};
struct singlelist
{
int num;//真实的数据
char buf[10]; //next指针
};
3、删除消息队列
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:cmd-->IPC_STAT 获取MSG的信息
IPC_SET 设置MSG的信息
IPC_RMID 删除MSG
buf-->存放信息结构体缓冲区
多线程:由进程生成,资源是进程给的,共享主线程资源,进程退出了,线程就没有了
1、线程的创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
返回值:成功 0 失败 errno
参数:thread -->线程的ID
attr -->线程的属性,一般设置为NULL表示使用默认属性
void *(*start_routine)(void *) -->线程的功能函数
arg -->传递给线程功能函数的参数
万能参数,什么类型都能传递
2、线程的退出跟回收
#include <pthread.h>
void pthread_exit(void *retval); --->在子线程调用
参数:retval -->存放线程退出时候的信息
int pthread_join(pthread_t thread, void **retval); //--->在主线程调用,回收子线程
参数:retval -->接收线程退出时候的信息
特点:阻塞主线程,等待子线程退出
3、线程的取消(干掉线程)
#include <pthread.h>
int pthread_cancel(pthread_t thread); //给指定线程发送取消请求
int pthread_setcancelstate(int state, int *oldstate); //设置线程的取消状态
参数:state ---> PTHREAD_CANCEL_ENABLE //线程默认的取消状态(延时取消)
PTHREAD_CANCEL_DISABLE
oldstate -->存放原本的取消状态,一般为NULL
int pthread_setcanceltype(int type, int *oldtype); //设置取消类型
参数:type -->PTHREAD_CANCEL_DEFERRED //延时取消:当线程被取消的时候,会继续执行到下一个取消点才退出,取消点:指的就是linux规定的一系列函数
PTHREAD_CANCEL_ASYNCHRONOUS //立即取消
4、线程的属性设置
#include <pthread.h>
第一步:定义属性变量pthread_attr_t,并初始化该变量
int pthread_attr_init(pthread_attr_t *attr);
第二步:依据你想要设置的某个具体属性选择合适的函数
int pthread_attr_setxxxxx();
第三步:让你设置的属性生效,通过创建线程实现
pthread_create( 传递属性变量)
第四步:线程结束,主线程中销毁
int pthread_attr_destroy(pthread_attr_t *attr);
第一个属性:分离属性
分离属性: 可以分离和不可以分离,可以分离意思就是线程不需要主函数去回收,自己回收自己(你强行回收也回收不到)-->自灭
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
参数:detachstate -->PTHREAD_CREATE_DETACHED //可分离
PTHREAD_CREATE_JOINABLE //不可以分离(默认属性)
第二个属性:设置优先级
SCHED_FIFO --->针对实时线程进程(静态优先级1--99)
SCHED_RR --->分配固定时间片,轮询
SCHED_OTHER --->针对非实时线程进程(静态优先级0),按照动态优先级来调度
互斥锁
用来协调多个线程对于共享资源(临界区资源)的访问,多个线程抢同一把锁,先抢到的不会阻塞,后来的线程抢不到锁会阻塞
死锁:某个线程上锁之后,没有解锁,导致其他线程无法使用该锁
1、互斥锁的初始化
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数:pthread_mutex_t -->互斥锁变量
pthread_mutexattr_t -->互斥锁的属性,一般设置为NULL,默认属性
2、互斥锁的操作
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); //阻塞上锁/加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);//非阻塞上锁,上锁操作不会阻塞
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
特点:多个线程争抢锁的时候,没抢到锁的会阻塞
3、处理线程取消时候导致的死锁
void pthread_cleanup_push(void (*routine)(void *),void *arg);//注册线程取消时候要执行的函数
参数:void (*routine)(void *) -->当线程被取消的时候要执行的函数
arg -->传递给routine的参数
void pthread_cleanup_pop(int execute);
参数:execute --->0表示routine出栈的时候不执行
非0表示routine出栈的时候执行
pthread_cleanup_pop(85);//85随便乱写的,非0,当调用该函数,会自动帮助你执行routine
4、 互斥锁的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
条件变量:必须配合互斥锁使用
1、条件变量的初始化
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t * attr);
参数:cond -->条件变量
attr -->条件变量的属性,一般始终为0
2、操作条件变量
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);//阻塞条件变量,会先解锁,然后阻塞当前线程
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有的阻塞在条件变量上线程
int pthread_cond_signal(pthread_cond_t *cond);//唤醒阻塞条件变量上某个线程,先解除阻塞,然后自动上锁
3、条件变量的销毁
int pthread_cond_destroy(pthread_cond_t *cond);
线程池
typedef struct task_list //任务链表
{
void *(*taskfun)(void *argu); //任务函数
void *argu; //任务参数
struct task_list *next;
}TASK_LIST,*TASK_LIST_POINT;
typedef struct pthread_pool //线程池
{
pthread_t *pid; //线程ID
int task_num; //线程池中任务个数
int pthread_num; //线程池中线程个数
pthread_mutex_t pthread_pool_mutext; //线程池锁
pthread_cond_t pthread_pool_cond; //线程池条件变量
struct task_list *task_list_head; //任务链表表头
int pthread_alive_flag; //0线程关闭,1打开
}PTHREAD_POOL,*PTHREAD_POOL_POINT;
1、任务链表初始化
TASK_LIST_POINT task_list_init() //初始化头结点
{
TASK_LIST_POINT task_list=malloc(sizeof(TASK_LIST));
if(task_list != NULL)
{
task_list->next = NULL;
}
return task_list;
}
2、线程从任务链表取出任务并执行
void *get_task(void *arg)//线程共用的功能函数:获取任务并执行
{
PTHREAD_POOL_POINT threadp=(PTHREAD_POOL_POINT)arg;
struct task_list *ptask;
//从任务链表的头部取出任务然后处理(说白了就是删除节点,处理完了再删除)
while(1)
{
pthread_mutex_lock(&(threadp->pthread_pool_mutext));//线程拿到任务先上锁
while(threadp->task_num<=0&&threadp->pthread_alive_flag==1)//任务数量为0且线程存在
{
pthread_cond_wait(&(threadp->pthread_pool_cond),&(threadp->pthread_pool_mutext));
}
if(threadp->task_num<=0&&threadp->pthread_alive_flag==0)//任务数量为0且线程不存在
{
pthread_mutex_unlock(&(threadp->pthread_pool_mutext));
pthread_exit(NULL);
}
//遍历任务链表,找到head的下一个节点,取出来(删除操作)
ptask = threadp->task_list_head->next;
threadp->task_list_head->next=ptask->next;
threadp->task_num--; //任务减1
pthread_mutex_unlock(&(threadp->pthread_pool_mutext));
(ptask->taskfun) (ptask->argu); //执行任务
free(ptask);
}
}
3、线程池初始化(包括线程池里线程个数)
PTHREAD_POOL_POINT pthread_pool_init(PTHREAD_POOL_POINT mypthread_pool,int pthread_num)//线程初始化
{
mypthread_pool->pid=malloc(pthread_num*sizeof(pthread_t));
mypthread_pool->pthread_num=pthread_num;
mypthread_pool->task_num=0;
mypthread_pool->task_list_head=malloc(sizeof(TASK_LIST));
mypthread_pool->task_list_head->next =NULL;
mypthread_pool->pthread_alive_flag=1;
pthread_mutex_init(&(mypthread_pool->pthread_pool_mutext),NULL);
pthread_cond_init(&(mypthread_pool->pthread_pool_cond),NULL);
for(int i=0; i<pthread_num; i++)//创建pthread_num个线程
{
pthread_create(&mypthread_pool->pid[i],NULL,get_task,mypthread_pool);//创建线程
}
return mypthread_pool;
}
4、添加任务
void task_add(void *(*sometask)(void *arg),void *somearg,PTHREAD_POOL_POINT mypthread_pool)//添加任务
{
//找到链表的尾部,封装新的节点,插入到尾部
TASK_LIST_POINT p=malloc(sizeof(TASK_LIST));
TASK_LIST_POINT newtask=malloc(sizeof(TASK_LIST));
p=mypthread_pool->task_list_head;
newtask->taskfun=sometask;
newtask->argu=somearg;
newtask->next = NULL;
pthread_mutex_lock(&(mypthread_pool->pthread_pool_mutext));
while(p->next != NULL)//操作指针尾部插入
p=p->next;
p->next=newtask;
mypthread_pool->task_num++;
pthread_mutex_unlock(&(mypthread_pool->pthread_pool_mutext));
pthread_cond_signal(&(mypthread_pool->pthread_pool_cond));//池中有任务时,唤醒线程去处理任务
}
5、线程池的销毁
int pthread_pool_destroy(PTHREAD_POOL_POINT mypthread_pool)//销毁线程池
{
mypthread_pool->pthread_alive_flag=0;
pthread_cond_broadcast(&(mypthread_pool->pthread_pool_cond));
for(int i=0; i<mypthread_pool->pthread_num; i++)
{
pthread_join(mypthread_pool->pid[i],NULL);
}
}
总结:多进程和多线程的区别
多进程优点:
每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系
通过增加CPU,就可以容易扩充性能
可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系
每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
多进程缺点:
逻辑控制复杂,需要和主程序交互
需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
多进程调度开销比较大
多线程优点:
无需跨进程边界
程序逻辑和控制方式简单
所有线程可以直接共享内存和变量
线程方式消耗的总资源比进程方式好
多线程缺点:
每个线程与主程序共用地址空间,受限于2GB地址空间
线程之间的同步和加锁控制比较麻烦
一个线程的崩溃可能影响到整个程序的稳定性
到达一定的线程数程度后,即使再增加CPU也无法提高性能
线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU