接着上一篇,这一篇我们来看看几个常见进程的实现,以及几个进程之间的优缺点和如何避免一些进程缺点出现。
1、孤儿进程:
父进程先于子进程结束。
实现过程:
2、创建守护进程:
**守护进程是指在后台运行且不与任何控制终端关联的进程。
守护进程是很有用的,在Linux的大多服务器都是守护进程的方式实现的,如Internet、Web服务器进程http等。
守护进程的启动方式有多种:它可以在Linux系统启动时从启动脚本/etc/rc.d中启动:可以由作业规划进程crond启动:还可以由用户终端(通常Shell)执行。编写守护进程的时候尽量避免产生不必要的交互。编写守护进程有以下要点:(仔细阅读)
(1)让进程在后台运行。方法是调用fork产生一个子进程,然后父进程退出。下面程序中会展现。
(2)调用setsid创建一个新对话期。终端控制,登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受他们的影响,其方法是调用setsid是进程成为一个会话组长。
注意:当进程是会话组长,调用setsid会失败,但第一点已经保证进程不是会话组长。setsid调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
(3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。威力避免这种情况的发生,可以通过是使进程不再是会话组长来实现。再一次通过fork创建新的子进程,使调用fork的进程退出。
(4)关闭不再需要的文件符。创建的子进程会继承父进程的文件描述符,不关闭会造成资源浪费,造成进程所在的文件系统无法卸下以及引起无法预料的错误。先得到最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
(5)将当前目录改为根目录。当守护进程当前工作目录在一个装配文件系统中时,该文件系统不能被拆卸。一般需要将工作目录改为根目录。
(6)将文件创建时使用的屏蔽字设置为0.进程从创建它的父进程那里继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用umask(0)将屏蔽字清零。
(7)处理SIGCHLD信号。这一步不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束子进程会成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程的结束将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束就不会产生僵尸进程。**
(以上说明君来自《linux c编程实战》,觉得描述很清就摘录过来了,希望对你有帮助)
下面我们来看看具体的守护进程的实现:
#include<iostream>
#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<syslog.h>//syslog函数头文件
#include<signal.h> /*信号头文件*/
#include<time.h>
#include<sys/stat.h>
#include<sys/param.h>
using namespace std;
using namespace std;
int deamon_init()
{
signal(SIGTHUP,SIG_IGN);
pid_t pid = fork();
if(pid == -1)
{
perror("fork:> ");
exit(1);
}
else if(pid)
exit(0);//结束父进程是子进程称为后台进程
if(setsid()<0)//建立新的进程组,在这个新的进程组中,子进程称为这个进程组的首进程,让该进程脱离所有终端,APUE第43也有详细说明
return -1;
pid = fork();
if(pid)
exit(0);
else if(pid == -1)
{
perror("fork1:> ");
exit(1);
}
for(int i=0; i<NOFILE; close(i++));//关闭所有从父进程继承下来的文件描述符
chdir("/");//改变工作目录,使得进程不与任何文件系统联系
umask(0);//将文件屏蔽字设为0
signal(SIGCHLD,SIG_IGN);
return 0;
}
int main()
{
time_t now;
deamon_init();
syslog(LOG_USER|LOG_INFO, "测试守护进程!\n");
while(1)
{
sleep(8);
time(&now);
syslog(LOG_USER|LOG_INFO, "系统时间:\t%s\t\t\n",ctime(&now));
}
reuturn 0;
}
使用syslog函数之前,首先需要配置/etc/syslog.conf文件,在该文件的最后一行user.* /var/log/test.log。然后重启syslog服务。
在这里也给大家推荐两本本书《UNIX网络编程》卷1:套接字联网API和《UNIX环境高级编程》,在这两本书里对守护进程过程有详细的介绍,尤其是在第二本书里。
3、僵进程:
通过简单的程序我们来看看降进程:
这种状态就是子进程结束父进程未结束的状态;
子进程称为僵死状态内核只保存该进程的一些必要信息一杯父进程所需,僵死状态会一直保持下去除非重启系统,
4、执行新进程:
使用fork和vfork创建子进程后,子进程通常会调用exec函数来执行另外一个程序。系统使用exec用于执行一个可执行程序以代替当前进程的执行影像。
注意:exec调用并没有生成新程序,一个进程一旦调用exec函数,它本身就”死亡“了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一保留的就是进程ID,也就是说,对系统而言,还是同一个进程,不过执行的已经是另外一个程序了。
我们来看看exec函数族的6个函数原型(也可以man 3 函数名查看):
#include<unistd.h>
int execve(const char* path, char *const argv[], char* const envp[]);
int execv(const char *path, char * const envp);
int execle(const char* path, const char* arg, ...);
int execl(ocnst char* path, const char* arg, ...);
int exevp(const char *file, char *const argv[]);
int execlp(const char *file, const char* arg, ...);
为了更好的理解exec函数族的使用,首先要理解环境变量这个概念。为了便于用户灵活地使用Shell,Linux引入了环境变量的概念,包括用户的主目录,终端类型、当前目录等,它们定义了用户的工作环境,所以称为环境变量。可以使用命令env查看环境变量值,用户也可以修改这些变量值以定制自己的工作环境。
我们用程序来看看环境变量:
也可以使用main函数来查看:
int main(int argc, char **argv, char **envp);
执行结果:
在用env命令我们会看到和程序输出是相通的:
事实上无论那个exec函数,都是将可执行程序的路径、命令行参数和环境变量3个参数传递给可执行程序的main函数。下面我们来介绍exec函数族成员是如何把main函数需要的参数传递过去的:
(1)execv函数:execv函数通过路径名方式调用可执行文件作为新的进程映像。它的argv参数用来提供给main函数的argv参数。argv参数是一个以NULL结尾(最后一个元素必须是一个空指针)的字符串数组。
(2)execve函数:在该系统调用中,参数path是将要执行的程序的路径名,参数argv、envp与main函数的argv、envp对应。
注意:参数argv、envp的大小都是受限制的。Linux操作系统通过宏ARG_MAX来限制他们的大小,如果他们的容量之和超过ARG_MAX定义的值将会发生错误。这个宏定义在头文件#include<linux/limits.h>
里。
(3)execl函数:该函数与execv函数用法类似。只是在传递argv参数的时候,每个命令行参数都声明为一个单独的参数(参数中使用“…”说明参数的个数是不确定的),要注意的是这些参数要以一个空指针作为结束。
(4)execle函数:该函数与execl函数用法类似。只要是显示执行环境变量。环境变量位与命令行参数最后一个参数的后面,也就是位与空指针之后。
(5)execvp函数:该函数和execv函数用法类似,不同的是参数filename。该参数如果包含“/”,就相当于路径名;如果不包括”/”,函数就在PATH环境变量定义的目录中寻找可执行文件。PATH环境变量的目录之间以冒号隔开。
(6)execlp函数:该函数类似于execl函数,它们的区别和execvp与execv的区别一样。
exec函数族的6个函数中只有execve是系统调用。其他5个都是库函数,它们实现时都调用execve。
正常情况下这些函数都是不会返回的,因为进程的执行映像已经被替换,没有接收返回值的地方了。如果有一个错误事件,将会返回-1,这些错误通常是由文件名或参数错误引起的。其含义如下图:
5、进程退出:
进程退出表示进程执行结束。在Linux系统中进程退出的方法有正常退出和异常退出;两种:
(1)正常退出:
在main函数中执行return,和执行exit作用一样;
调用exit;
调用_exit或者_Exit函数;
(2)异常退出:
调用about函数;
进程收到某个信号,而该信号是程序终止。
不管是那种退出方式,最终都会执行内核中的同一段代码。这段代码用来关闭进程所有打开的文件描述符,释放它所占用的内存和其他资源。
各类退出方式比较:
exit和return:exit是一个函数,有参数;return是函数执行完成后的返回。exit把控制权交给系统,而return把控制权交个调用函数。
exit和about的区别:exit是正常终止进程,而about是异常终止。
exit和_exit或_Exit:exit在头文件stdlib.h中声明,而_exit 和 _Exit在头文件unistd.h中,两个函数均能正常终止进程,但_exit()和_Exit执行后会立即返回给内核,而exit要先执行一些清除操作,饭后将控制权交给内核。