进程编程
进程基本概念
定义
- 进程是描述程序执行过程和资源共享的基本单位
- 目的:控制和协调程序的执行。
进程相关函数
- 创建:system() 、fork() 、 exex()
- 终止:kill()
- 等待进程终止:wait() 、waitpid()
父子进程之间的关系
关于资源:子进程得到的是除了代码段是与父进程共享的以外,其他所有的都是得到父进程的一个副本,子进程的所有资源都继承父进程,得到父进程资源的副本,既然为副本,也就是说,二者并不共享地址空间。,两个是单独的进程,继承了以后二者就没有什么关联了,子进程单独运行。(采用写时复制技术)
关于文件描述符:继承父进程的文件描述符时,相当于调用了dup函数,父子进程共享文件表项,即共同操作同一个文件,一个进程修改了文件,另一个进程也知道此文件被修改了。
进程组
- 定义:由一个或多个相关联的进程组成,目的是为了进行作业控制。
- 特征:信号可以发送给进程组中的所有进程,并使该进程组中所有进程终止、停止或继续运行。
- 每个进程都属于某个进程组
进程组函数
- 获取进程组ID: pid_t getpgid(pid_t pid)
返回pid进程的进程组ID;若pid为0,返回ID;出错返回-1,并设errno值。
设置进程组ID: intsetpgid(pid_t pid, pig_t pgid);
- 若pid为0,则使用调用者PID;若pgid为0,则将pid进程的进程PID设为进程组ID; 成功 0;错-1,设errno值。
会话(session)
- 为一个或多个进程组的集合,包括登录用户的全部活动,并具有一个控制终端。
- 登录进程为每个用户创建一个会话,用户登录shell进程成为会话首领,其PID设为会话ID
- 非会话首领进程通过调用setsid()函数创建新会话,并成为首领。
进程组函数
- 获取会话ID:pid_t getsid(pid_t pid);
- 返回pid 进程的会话ID;若pid为0,返回会话ID;出错返回-1,设errno值。
- 设置会话ID: pid_t setsid();
信号
信号(signal):进程通讯机制
- 信号是发送给进程的特殊异步消息
- 进程接收到消息时立即处理,此时并不需要完成当前函数调用甚至当前代码行。
- Linux系统中以数字标识不同的信号,程序以名称引用之。
系统信号
- 缺省处理逻辑:终止进程,生成内核转储文件
- 使用 “kill -l” 可查看系统支持的信号列表。
进程间发送的信号
- SIGTERM 、SIGKILL :终止进程信号,前者是请求,后者是强制。
- SIGUSR1 、SIGUSR2:用户自定义信号,向进程发送命令。
信号处理
- 进程接收到信号后,根据信号配置进行处理
- 缺省配置:在程序没有处理时,确定信号该如何处理
- 处理方式:按照信号处理例程的函数指针类型定义一个函数,然后调用。
sigaction()函数:设置信号配置
- 原型: intsigaction( int signum, const struct sigaction* act ,struct sigaction* oldact) ;
- signum 为信号编号,act和oldact分别为指向信号结构体struct sigaction的指针,前者为新配置,后者为需要保存的老配置
信号结构体 struct sigaction
- 最重要的成员 sa_handler ,取值为SIG_DFL(使用信号缺省配置),SIG_IGN(忽略该信号)或指向信号处理例程的函数指针(以信号编号为参数,无返回值)
信号处理时的注意事项
- 信号是异步操作,处理时主程序非常脆弱。
- 信号处理例程尽可能短小
- 不要在信号处理例程中实施 I/O 操作,也不要频繁调用系统函数或库函数。
- 不进行复杂的赋值操作
- 使用 sig_atomic_t类型的全局变量赋值。
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
sig_atomic_t sigusr_count = 0;
extern"C" {void OnSigUsr1(int signal_number)//定义外部c函数,信号处理例程。
{ ++sigusr1_count;}
}
int main(){
std::cout<<"pid:"<<(int)getpid()<<std::endl;
memset(&sa,0,sizeof(sa));//设置内存,把内存清零
sa.sa_handler = &OnSigUsr1;//设置sa_handler为OnSigUsr1的用户地址
sigaction(SIGUSR1, &sa,NULL);//把&sa挂到SIGUSR1这个信号上
sleep(100);//在终端中输入kill -s SIGUSR1 pid ,信号计数器将增加
std::cout<<"SIGUSR1 counts:"<<sigusr1_count<<std::endl;
return 0;
}
进程管理
进程创建
system( )函数:用于在程序中执行一条命令
- 原型:
int system(const char* cmd);
- 示例:
int ret_val = system("ls -l");
fork( )函数:创建当前进程的副本作为子进程。
- 原型:
pid_t fork();
- 返回两个值:返回值为0(新创建的子进程)和子进程的PID(父进程);
执行命令
exec( )函数:
基本模式: 在程序中调用fork( )创建一个子进程,然后调用exec( )在子进程中执行命令
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
int spawn(char* program,char** args);
int main(){
char* args[] = {"ls","-l","/",NULL};
spawn("ls",args);
cout << "Done!\n";
return 0;
}
//创建一个子进程运行新程序
//program为程序名,arg_list为程序的参数列表;返回值为子进程id
int spawn(char* program,char** args){
pid_t child_pid = fork(); //复制进程
if(child != o) //此为父进程
return child_pid;
else{ //此为子进程
execvp(program, args); //执行程序,按路径查找
//只有发生错误时,该函数才返回
std::cerr<<"Error occurred when executing execvp.\n";
abort();
}
}
进程调度
进程调度策略:先进先出,时间片轮转 ,普通调度, 批调度 ,高优先级抢先
子进程与父进程的调度没有固定顺序;不能假设子进程在父进程后执行,也不能假设子进程在父进程之前结束。
扫描二维码关注公众号,回复: 926752 查看本文章进程调度策略函数:头文件 “sched.h”
进程优先级调整:头文件“sys/time.h” 和 “sys/resource.h”
处理器亲和性:头文件 “sched.h”
进程终止
终止进程函数:kill( )
- 头文件 “sys/types.h” 和“signal.h”
- 原型: int kill(pid_t ,int sig);
- 函数参数: pid为子进程ID,sig应为进程终止信号SIGTERM
等待进程结束函数: wait( )
- 原型: pid_t wait(int* status)(等待子进程结束);pid_t waitpid(pid_t pid, int* status, int options);(等待特定进程结束)
- 阻塞主调过程,直到一个子进程结束
- WEXITSTATUS宏:查看子进程的退出码
- WIFEXITED宏:确定子进程的退出状态是正常退出,还是未处理信号导致的意外死亡。
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h> //必须包含,否则与wait共用体冲突
#include <unistd.h>
int spawn(char* program,char** args_list);
int main(){
char* arg_list[] = {"ls","-l","/",NULL};
spawn("ls",arg_list);
int child_status;
wait(&child_status); //等待子进程结束
if(WIFEXITED(child_status)) //判断子进程是否退出
cout<<"Exited normally"<<WEXITSTATUS(child_status)<<endl;
else
cout<<"Exited abnormally"<<endl;
cout << "Done!\n";
return 0;
}
僵尸进程
子进程已结束,但父进程未调用 wait( ) 函数等待
- 子进程已终止,但没有被正确清除,成为僵尸进程
清除子进程的手段:
- 父进程调用wait( )函数
- 即使在父进程调用wait( )函数前已死亡,其退出状态也会被抽取出来,然后被清除
- 未清除的子进程自动被 init 进程收养
子进程异步清除
SIGCHILD信号:子进程终止时,向父进程自动发送,编写例程,异步清除子程序
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
sig_atomic_t child_exit_status;
extern "C"{
void ClearnUp(int sif_num){
int status;
wait(&status); //清除子进程
child_exit_status = status; //存储子进程的状态
}
}
int main(){
//处理SIGCHLD信号
struct sigaction sa;
memset(&sa,0,sizeof(sa));
sa.sa_handler = &CleanUp;
sigaction(SIGCHLD,&sa,NULL);
//正产处理代码在此,例如调用fork()创建子进程
return 0;
}
守护进程
Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
守护进程的名称通常以d结尾,比如sshd、xinetd、crond等
守护创建步骤:
- 创建新进程:新进程将成为未来的守护进程
- 守护进程的父进程退出:保证祖父进程确认父进程已结束,且守护进程不是组长进程
- 守护进程创建新进程和新会话:并成为两者的首进程,此时刚创建的新会话还没有关联控制终端
- 改变工作目录:守护进程一般随系统启动,工作目录不应继续使用继承的工作目录
- 重设文件权限掩码:不需要继承文件权限掩码
- 关闭所有文件描述符:不需要继承任何打开的文件描述符
- 标准流重定向到/dev/null
守护进程创建函数daemon( )
- 原型:int daemon(int nochdir,int noclose);
- 参数:nochdir非0,不更改工作目录;noclose非0,不关闭所有打开的文件描述符;一般均设 0 ;
- 返回值:成功返回0;失败返回 -1,并设置 errno 值;
进程间通讯
管道:相关进程间的顺序通信
进程信号量:进程间通信的同步控制机制
共享内存:允许多个进程读写同一片内存区域
映射内存:与共享内存意义相同,但与文件相关联
消息队列:在进程间传递二进制块数据
套接字:支持无关进程,甚至不同计算机进行通信
管道(pipe)
性质与意义:
- 管道是允许单向通信的自动同步设备(半双工)
- 数据在写入端写入,在读取端读取
- 管道为串行设备,数据读取顺序与写入顺序相同
管道用途:
- 只能用于亲缘关系进程,比如父子进程间通信
注意:
- 管道数据容量有限,一般为一个内存页面
- 若写速超过读速,写进程阻塞,直到容量有空闲
- 若读速超过写速,读进程阻塞,直到管道有数据
pipe函数:创建管道
- 头文件:”unistd.h” 和 “fcntl.h”f
- 原型:
int pipe(int pipefd[2])
- 参数:包含两个元素的整数数组,类型为文件描述符, 0号元为读取文件描述符, 1号元为写入文件描述符
- 返回值:成功 0;失败 -1,设置 errno 值
管道重定向
等位文件描述符
- 共享相同的文件位置和状态标志设置
dup( )函数:将两个文件描述符等位处理
- 原型:int dup (int oldfd) ; int dup2(int oldfd, int newfd);”
- 参数:创建oldfd的一份拷贝,单参数版本选择数量值最小的未用的文件描述符作为新的文件描述符;双参数版选择 newfd 作为新的文件描述符,拷贝前尝试关闭 newfd;
- 返回值:成功,返回文件描述;失败 -1;
- 示例:dup2(fd , STDIN_FILENO) 关闭标准输入流,然后作为 fd 的副本重新打开
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
const int buf_size = 4096;
int main(){
int fds[2];
pipe(fds); //创建管道
pid_t pid = fork();
if(pid == (pid_t)0){ //子进程
close(fds[0]); //关闭管道读取端
dup2(fds[1],STDOUT_FILENO); // 管道挂接到标准输出流
char* args[] = {"ls","-l","/",NULL}; // 使用 "ls" 命令替换子进程
execvp(args[0],args);
}
else{ //父进程
close(fds[1]); //关闭管道写入端
char buf[buf_size];
FILE* stream = fdopen(fds[0],"r"); //以读模式打开管道读取端,返回文件指针
fprintf(stdout,"Data received:\n");
//在流未结束,未发生读取错误,且能从流中正确读取字符串时,输出读取到的字符串
while(!feof(stream) && !ferror(stream) && fgets(buf,sizeof(buf),stream) != NULL){
fputs(buf,stdout);
}
close(fds[0]); //关闭管道读取端
waitpid(pid,NULL,0); //等待子进程结束
}
return 0;
}
进程信号量
进程信号量:System V信号量
- 可以使用同步机制确定进程的先后执行顺序
- 头文件: “sys/types.h” , “sys/ipc.h ” , “sys/sem.h”
定义:
- 信号量是一类特殊的计数器,值为非负整数,用于进程或线程同步
信号量的操作
- 等待(wait)操作(P):信号量的值递减 1 后返回;若为 0 ,阻塞操作,直到为正,然后减 1 返回;
- 发布(post)操作(V):信号量值递增 1 后返回; 若为 0 ,则其中一个等待该信号量的进程或线程取消阻塞
Linux信号量实现:两个版本
- 进程信号量用于进程同步;POSIX标准实现用于线程同步
注意事项:
- 每次创建和管理的进程信号量是一个集合(数组),可能包含多个进程信号量
- 使用键值 key 关联进程信号量集,但进程信号量本身由标识符 semid 标识(函数调用时使用) : semid 对内, key 对外;
获取进程信号量
semget( )函数:创建或获取进程信号量集
控制进程信号量
int semctl(int semid,int semnum, int cmd, …)
清除进程信号量
IPC_RMID
- 最后一个使用进程信号量的进程负责清除进程信号量集
- 进程信号量集释放后,内存自动释放
等待与发布
semop( )
- 原型: int semop(int semid, struct sembuf* sops, size_t nsops);
//P原语:等待二元信号量,信号数非正时阻塞
int WaitBinarySemaphore(int semid)
{
struct sembuf ops[1];
ops[0].sem_num = 0;
ops[0].sem_op = -1;
ops[0].sem_flg = SEM_UNDO;
return semop(semid,ops,1);
}
//V原语:发布二元信号量,增加信号数后立即返回
int WaitBinarySemaphore(int semid)
{
struct sembuf ops[1];
ops[0].sem_num = 0;
ops[0].sem_op = 1;
ops[0].sem_flg = SEM_UNDO;
return semop(semid,ops,1);
}
共享内存
意义:快捷方便的本地通讯机制
- 头文件: “sys/ipc.h” , “sys/shm.h”
共享内存编程原则:
- 系统没有对共享内存操作提供任何缺省同步行为
- 若需要,则自主设计:使用进程信号量
使用过程:
- 某个进程分配一个内存段,其他需要访问该内存段的进程连接(attach)该内存段
- 完成访问后,进程拆卸(detach)该内存段
- 某个时刻,一个进程释放该内存段
映射内存
mmap()函数:头文件”sys/mman.h”
- 原型: void* mmap(void* addr,size_t length, int prot , int flags ,int fd , off_t offset) ;
- 映射共享文件到内存;
- 文件被分割成页面大小装载;
- 使用内存读写操作访问文件,操作更快;
- 对映射内存的写入自动反映到文件中;
munmap()函数:释放映射内存
- 原型: int* munmap(void* addr , size_t length);
消息队列
在两个进程间传送二进制块数据
- 数据块具有类别信息,接收方可根据消息类别有选择地接收
- 头文件:”sys/type.h” , “sys/ipc.h” , “sys/msg.h”
msgget( )函数:创建或获取消息队列
- 原型: int msgget(ket_t key , int msgflg );
msgsnd()函数:将消息添加到消息队列中
- 原型: int msgsnd(int msqid , const void* msg_ptr , size_t msg_sz , int msgflg );
msgrcv()函数:从消息队列中获取消息
- 原型: int msgrcv(int msqid , void* msg_ptr , size_t msg_sz , long int msgtype , int msgflg);
msgctl()函数:控制消息队列的某些属性
- 原型:int msgctl ( int msgqid , int cmd , struct msqid_ds* buf )
进程池
动机:为什么需要引入线程池
- 进程需要频繁创建子进程,以执行特定任务
- 动态创建子进程过程效率较低,客户响应速度较慢
- 动态创建的子进程一般只为单一客户提供服务,当客户较多时,系统中会存在大量子进程,进程切换开销过高
- 动态创建的子进程为当前进程的完整映像,当前进程必须谨慎管理系统资源,防止子进程不适当地复制这些资源
什么是线程池?
- 主进程预先创建一组子进程,并统一管理
- 子进程运行同样代码,具有同样属性,个数多与CPU数目一致,很少超过CPU数目的两倍
进程池工作原理:
- 主进程充当服务器,子进程充当服务员,按照服务器的需要提供服务
- 当任务到达时,主进程选择一个子进程进行服务
- 相对于动态创建子进程,选择的代价显然更小 —— 这些子进程未来还可以被复用
- 存在多种选择子进程的策略,如随机选择或轮值制度,如共享单一任务队列,还可以使用更加智能化的负载平衡技术
- 父子进程之间应该有传递信息的通道,如管道、共享内存、消息队列等,也可能需要同步机制