Linux 多进程编程
Linux多进程编程
1、进程相关概念
程序和进程
并发:在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任一时刻点上仍只有一个进程在运行。(实质上,并发是宏观并行,微观串行)
单道程序设计:所有进程一个一个排对执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发
展历史上存在不久,大部分便被淘汰了。
多道程序设计:在计算机内存中同时存放几道相互独立的程序,他们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证
时钟中断 即为多道程序设计模型的理论基础。并发时,任意进程在执行期问都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。操作 系统中的中断处理函数,来负贵调度程序执行。在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
CPU和MMU:
虚拟内存和物理内存映射关系:MMU(内存管理单元)位于CPU内,1)完成虚拟内存和物理内存映射; 2)设置修改内存访问级别。
进程控制块PCB:
每个进程在内核中都有一个进程控制块(pcb)来维护进程相关的信息,Linux内核进程控制块是task_struct结构体( /usr/src/inux-headers-3.16.0 30/include/linux/sched.h )
-
进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示(非负整数)
-
进程的状态,有初始态、就绪态、运行态、挂起态、终止态等状态。
-
进程优先级
-
描述虚拟地址空间的信息。描述控制终端的信息。
-
进程切换时需要保存和恢复的一些CPU寄存器。
-
当前工作目录(Current Working Directory)。
-
umask掩码。
-
资源清单
-
队列指针
-
文件描述符表,包含很多指向file结构体的指针。
-
和信号相关的信息。
-
用户id和组id。
-
会话(Session) 和进程组。
-
进程可以使用的资源上限(Resource Limit)。 。
2、环境变量
PATH:可执行文件的搜索路径。(查看: echo $PATH)
SHELL:当前Shell,它的值通常是 /bin/bash。
TERM:(终端)当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一 般不行。
LANG:语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME:当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。该程序时都有自己的一套配置。
3、进程控制
1)fork函数
创建一个子进程。fork被调用一次,但返回两次(子进程返回0,父进程返回子进程ID)。
fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
pid_t fork(void); 失败返回-1; 成功返回: ①父进程返回子进程的ID(非负)②
子进程返回0。
pid_t 类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,
init。最小,为1)
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
using namespace std;
int main(int argc, char* argv[])
{
cout << "before fork-1-" << endl;
cout << "before fork-2-" << endl;
cout << "before fork-3-" << endl;
cout << "before fork-4-" << endl;
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
cout << "---child is create "<< endl;
}
else if(pid > 0)
{
cout << "---parent process: my child is " << pid << endl;
}
cout << "===================end of file"<< endl;
return 0;
}
getpid() / getppid() / getuid() / getgid() 函数
作用:
getpid() :获取当前进程的id。
getppid() :获取父进程的id。
getuid() :获取当前进程实际用户id。
getgid() :获取当前进程使用用户组id。
声明:
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
uid_t getuid(void);
uid_t geteuid(void); //获取当前进程有效用户id。
uid_t getgid(void);
uid_t getegid(void); //获取当前进程有效用户组id。
示例:
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
using namespace std;
int main(int argc, char* argv[])
{
cout << "before fork-1-" << endl;
cout << "before fork-2-" << endl;
cout << "before fork-3-" << endl;
cout << "before fork-4-" << endl;
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
cout << "---child is create, pid = "<< getpid() << ",parent-pid:"<< getppid() << endl;
}
else if(pid > 0)
{
cout << "---parent process: my child is " << pid << ",my pid:" << getpid() << ",my parent pid:" <<getppid() << endl;
}
cout << "===================end of file"<< endl;
return 0;
}
循环创建多个子进程
进程共享
父子相同处:
全局变量、.data、.text、权、堆、环境变量、用户ID、宿主目录、进程工
作目录、信号处理方式..
父子不同处:
1.进程ID 2.fork返回值 3.父进程 ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集。
父子间共享:
读时共享写时复制。---------全局变量
1、文件描述符 2、mmap映射区
父子进程间遵循读时共享写时复制的原则。这样的设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
2)exec 函数族
概念:
exec函数族提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。
作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
函数原型:
#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 execvpe(const char *file, char *const argv[],char *const envp[]);
execl execlp 函数
int execlp(const char *file, const char *arg, ...);
p:使用文件名,并从PATH环境进行寻找可执行文件(加载一个进程,借助PATH环境变量) 。
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
using namespace std;
int main(int argc, char* argv[])
{
pid_t pid = fork(); //创建子进程
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0) //子进程
{
//execlp("ls","-l","-d","-h",NULL); //错误的写法
//execlp("ls","ls","-l","-h",NULL); //正确
//execlp("date","date",NULL); //若成功,则后面两行不执行
execl("./a.out","./a.out",NULL);
perror("exec error");
exit(1);
}
else if(pid > 0) //父进程
{
sleep(1);
cout << "I am parent :" << getpid() << endl;
}
return 0;
}
3)回收子进程
孤儿进程:
父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init进程,称init进程领养孤儿进程。
僵尸进程:
进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
特别注意:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。(清除僵尸进程:kill 父进程)
①wait函数
功能:
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)
pid_t wait (int *status);
成功:清除掉的子进程ID;失败:-1(没有子进程)
当进程终止时,操作系统的隐式回收机制会: 1.关闭所有文件描述符2.释放用户空间
分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常
终止→终止信号)。可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数 :
常用宏函数分为日如下几组:
1、 WIFEXITED(status) 非0 进程正常结束
若上宏为真,此时可通过WEXITSTATUS(status)获取进程退出状态(exit时参数)
if(WIFEXITED(status)){
printf("退出值为 %d\n", WEXITSTATUS(status));
}
2、 `WIFSIGNALED(status) 非0 进程异常终止
若上宏为真,此时可通过WTERMSIG(status)获取使得进程退出的信号编号
用法示例:
if(WIFSIGNALED(status)){
printf("使得进程终止的信号编号: %d\n",WTERMSIG(status));
}
3、 WIFSTOPPED(status) 非0 进程处于暂停状态
若上宏为真,此时可通过WSTOPSIG(status)获取使得进程暂停的信号编号
4、 WIFCONTINUED(status) 非0 表示暂停后已经继续运行
示例:
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
int main(void)
{
pid_t pid, wpid;
int status;
pid = fork();
if(pid == 0)
{
printf("---child,my id= %d,going to sleep 10s\n",getpid());
sleep(10);
printf("------------child die------------\n");
return 99; //特殊值
}
else if(pid > 0)
{
wpid = wait(&status); //如果子进程未终止,父进程阻塞在这个函数上
if(wpid == -1)
{
perror("wait error");
exit(1);
}
if(WIFEXITED(status)) //为真,说明子进程正常终止
{
printf("child exit with %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status)) //为真,说明子进程是被信号终止
{
printf("child kill with signal %d\n",WTERMSIG(status));
}
printf("-------------parent wait finish:%d\n",wpid);
}
else
{
perror("fork");
return 1;
}
return 0;
}
②waitpid 函数
作用同wait,但可指定pid进程清理,可以不阻塞
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
成功:返回清理掉的子进程ID;失败:-1(无子进程)。
参数值 | 说明 |
---|---|
pid<-1 | 等待进程组号为pid绝对值的任何子进程。 |
pid=-1 | 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。 |
pid=0 | 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。 |
pid>0 | 等待进程号为pid的子进程。 |
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
*int *status*
这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常推出还是出了什么错误。如果status不是空指针,则状态信息将被写入器指向的位置。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
Linux提供了一些非常有用的宏来帮助解析这个状态信息,这些宏都定义在sys/wait.h头文件中。主要有以下几个:
宏 | 说明 |
---|---|
WIFEXITED(status) | 如果子进程正常结束,它就返回真;否则返回假。 |
WEXITSTATUS(status) | 如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。 |
WIFSIGNALED(status) | 如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。 |
WTERMSIG(status) | 如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。 |
WIFSTOPPED(status) | 如果当前子进程被暂停了,则返回真;否则返回假。 |
WSTOPSIG(status) | 如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。 |
int options
参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。
主要使用的有以下两个选项:
参数 | 说明 |
---|---|
WNOHANG | 如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。 |
WUNTRACED | 如果子进程进入暂停状态,则马上返回。 |
这些参数可以用“|”运算符连接起来使用。
如果waitpid()函数执行成功,则返回子进程的进程号;如果有错误发生,则返回-1,并且将失败的原因存放在errno变量中。
失败的原因主要有:没有子进程(errno设置为ECHILD),调用被某个信号中断(errno设置为EINTR)或选项参数无效(errno设置为EINVAL)
如果像这样调用waitpid函数:waitpid(-1, status, 0),这此时waitpid()函数就完全退化成了wait()函数。
4、守护进程
进程分类:前台进程、后台进程、守护进程
1) 守护进程:
守护进程也叫Deamon进程(精灵进程),是一种特殊的进程,一般在后台运行,不与任何控制终端相关联,并且周期性地执行某种任务或等待处理某些发生的事件(处理一些系统级的任务)。
守护进程通常在系统启动时就运行,它们以 root 用户或者其他特殊的用户运行(例如 apache)。
常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
特点:
- 后台运行,不与终端关联(属于特殊的孤儿进程)
- 运行周期长,守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。
- 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。
- 守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
- 守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
- 习惯上守护进程的名字通常以 d 结尾(如 httpd, sshd),但这不是强制要求的。
2) 会话
每打开一个控制终端,或者在用户登录时,系统就会创建新会话。
每个会话通常都与一个控制终端相关联。
在该会话中运行的第一个进程称作会话首进程,通常这个首进程就是shell。
#include<unistd.h>
pid_t setsid(void); //创建新会话,当前调用进程不能是组长进程
pid_t getsid(pid_t pid);//获取进程所属会话id,pid=0是代表当前进程
setsid
函数用于创建一个新会话,并担任该会话的组长,作用:
- 让进程摆脱①原会话②原进程组③原控制终端 的控制
3) 进程组
每个进程都属于某个进程组,进程组是由一个或多个相互间有关联的进程组成的,它的目的是为了进行作业控制。
进程组的主要特征就是信号可以发给进程组中的所有进程:这个信号可以使同一个进程组中的所有进程终止、停止或者继续运行。
每个进程组都由进程组 id 唯一标识,并且有一个组长进程。进程组 id 就是组长进程的 pid。只要在某个进程组中还有一个进程存在,则该进程组就存在。
即使组长进程终止了,该 #include pid_t setsid(void);创建新会话,当前调用进程不能是组长进程 pid_t getsid(pid_t pid); 获取进程所属会话 id, pid=0 是代表当前进程进程组依然存在。
#include<unistd.h>
int setpid(pid_t pid, pid_t pgid); //设置进程的组id
pid_t getpid(pid_t pid); //获取进程的组id
pid_t getpgrp(void); //另一种方式设置进程组id
4) 守护进程编写步骤
①fork创建子进程,父进程退出(必须)
- 所有工作在子进程中进行形式上脱离控制终端
②在子进程中创建新会话(必须)
- setsid( ) 函数
- 使子进程完全独立出来,脱离控制
③改变当前目录为根目录(不是必须)
- chdir() 函数
- 防止占用可卸载的文件路径
- 也可以换成其他路径
④重设文件权限掩码(不是必须)
- umask() 函数
- 防止继承的文件创建屏蔽字拒绝某些权限
- 增加守护进程灵活性
⑤关闭文件描述符(不是必须)
- 继承的打开文件不会用到,浪费系统资源,无法卸载
for(i=0; i<MAXFILE; i++)
close(i);
⑥开始执行守护进程核心工作(必须)
- 后湖进程退出处理程序模型
signal(SIGTERM, sigterm_handler);
void sigterm_handler(int arg)
{
-
_running = 0
}
实现代码:
实现:隔10秒在 /tmp/dameon.log 文件中写入一句话
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
#include<sys/stat.h>
#define MAXFILE 65535
volatile sig_atomic_t _running = 1;
void sigterm_handler(int arg)
{
_running = 0;
}
int main()
{
pid_t pc;
int i, fd, len;
char *buf = "this is a Dameon\n";
len = strlen(buf);
pc = fork(); //第一步
if(pc < 0)
{
printf("error fork\n");
exit(1);
}
else if(pc > 0)
{
exit(0);
}
setsid(); //第二步
chdir("/"); //第三步
umask(0); //第四步
for(i = 0; i < MAXFILE; i++) //第五步
{
close(i);
}
signal(SIGTERM, sigterm_handler);
while (_running)
{
if((fd = open("/tmp/dameon.log",O_CREAT | O_WRONLY | O_APPEND, 0600)) < 0)
{
perror("open");
exit(i);
}
write(fd, buf, len + 1);
close(fd);
usleep(10 * 1000); //10毫秒
}
}