首页 > 代码库 > Linux 程序设计学习笔记----进程管理与程序开发(下)

Linux 程序设计学习笔记----进程管理与程序开发(下)

转载请注明出处:http://blog.csdn.net/suool/article/details/38419983,谢谢!


进程管理及其控制

创建进程

fork()函数

函数说明具体参见:http://pubs.opengroup.org/onlinepubs/009695399/functions/fork.html

返回值:Upon successful completion, fork() shall return 0 to the child process and shall return the process ID of the childprocess to the parent process. Both processes shall continue to execute from thefork() function. Otherwise, -1 shall bereturned to the parent process, no child process shall be created, anderrno shall be set to indicate the error.

fork()函数调用成功后,将为子进程申请PCB和用户内存空间。子进程会复制父进程的几乎所有信息,在用户空间内将复制所有数据(代码段、数据段、BSS、堆、栈),复制父进程的内核空间PCB的绝大多数信息。子进程从父进程继承下列属性:有效用户/组号,进程组号,环境变量,对文件的执行时的关闭标志,信号处理方式设置,信号屏蔽集合,当前工作目录,根目录,文件掩码格式,文件大小限制,打开的文件描述符。

创建子进程示例:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(int argc,char *argv[])
{
	pid_t pid;
	if((pid=fork())==-1)          // 创建子进程,在父进程运行
		printf("fork error");
	printf("bye!\n");        // 父子进程都将执行这段
	return 0;
}

Result:

下面也是一个:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
	pid_t pid;
	if((pid=fork())==-1)   // 创建子进程
		printf("fork error");
	else if(pid==0)
	{
		printf("in the child process\n");  // 在子进程中有运行的代码
	}
	else
	{
		printf("in the parent process\n"); // 在父进程中运行的代码
	}
	return 0; // 父子进程都会返回
}

Result:


在前面的学习中,我们知道,文件缓冲区的资源在用户空间区,因此,创建的子进程的用户空间将复制父进程的用户空间的所有信息,显然包括缓冲流的信息。如父进程缓冲流中有信息,同样会复制到子进程的用户空间缓冲流中。

如下:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
	pid_t pid;
	if((pid=fork())==-1)   // 创建子进程
		printf("fork error");
	else if(pid==0)
	{
		printf("in the child process\n");  // 在子进程中有运行的代码
	}
	else
	{
		printf("in the parent process\n"); // 在父进程中运行的代码
	}
	return 0; // 父子进程都会返回
}


子进程对父进程打开的文件描述符的处理

创建子进程后父子进程对打开文件的处理方式如下:


示例代码如下:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
    pid_t pid;
    int fd;
    int i=1;
    int status;
    char *ch1="hello";
    char *ch2="world";
    char *ch3="IN";
    if((fd=open("test.txt",O_RDWR|O_CREAT,0644))==-1)  // 以O_CREAT方式打开一个文件
    {
	perror("parent open");
	exit(EXIT_FAILURE);
    }
    if(write(fd,ch1,strlen(ch1))==-1)    // 父进程向文件写数据
    {	 
	perror("parent write");
	exit(EXIT_FAILURE);
    }
    
    if((pid=fork())==-1)                // 创建新进程
    {
	perror("fork");
	exit(EXIT_FAILURE);
    }
    else if(pid==0)                     // 子进程
    {
	i=2;	
	printf("in child\n");
	printf("i=%d\n",i);             // 打印i值
	if(write(fd,ch2,strlen(ch2))==-1)  // 写文件,与父进程共享
	    perror("child write");        
	return 0;
    }
    else            // 父进程
    {
	sleep(1);
	printf("in parent\n");
	printf("i=%d\n",i);      // 打印i值,对比子进程
	if(write(fd,ch3,strlen(ch3))==-1)  // 写操作,结果添加到尾部
	    perror("parent,write");
	wait(&status);        // 等待进程结束
	return 0;
    }
}

运行结果:


从上面可以看出,父子进程共享文件表项,不会交叉覆盖写入,共享文件偏移。

vfork()函数

vfork()创建子进程的时候并不复制父进程的地址空间,而是在有必要的时候才会复制。如果子进程执行exec()函数,则使用fork()函数从父进程复制到子进程的数据空间不再使用。这样效率很低,从而使vfork()函数非常有用。根据父进程数据空间的大小,vfork()比fork()可以很大的程度上提高性能。vfork()只在需要的时候复制,而一般采用与父进程共享所有的资源的方式。

具体参见:http://pubs.opengroup.org/onlinepubs/009695399/functions/vfork.html

执行的过程中,fork()和vfork()函数有一定的区别,fork()函数是复制一个父进程的副本,从而拥有自己的独立的代码段、数据段、以及堆栈空间,即成为一个独立实体。而vfork()是共享父进程的代码和数据段。

下面程序示例而这区别:

#include<unistd.h>
#include<error.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
int glob=6;            // 全局已经初始化的变量,位于数据段中
int main()
{
	int var;
	pid_t pid;
	var=88;        // 局部变量,位于栈空间
	printf("in beginning:\tglob=%d\tvar=%d\n",glob,var); // 打印全局变量,局部变量
	if((pid=vfork())<0)
	{
		perror("vfork");
		exit(EXIT_FAILURE);
	}
	else if(pid==0)    // 子进程
	{
		printf("in child,modify the var:glob++,var++\n");
		glob++;     // 子进程修改全局变量
		var++;      // 子进程中的修改局部变量
		printf("in child:\tglob=%d\tvar=%d\n",glob,var);
		_exit(0);   // 使用_exit()退出
	}
	else     // 父进程打印两变量
	{	
		printf("in parent:\tglob=%d\tvar=%d\n",glob,var);
		return 0;
	}
}

编译运行结果:

现在将上面的程序中的vfork改为fork()函数,运行结果是:

子函数调用vfork()函数创建子进程

下面是一个示例:

先贴出运行结果:

显然出现了段错误。

代码如下:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void test()          // 在此函数中调用vfork()函数
{
    pid_t pid;
    pid=vfork();
    if(pid==-1)
    {
       perror("vfork");
       exit(EXIT_FAILURE);
    }
    else if(pid==0)   // 子进程中打印进程信息返回,从结果看,可以正常运行
    {   
       printf("1:child pid=%d,ppid=%d\n",getpid(),getppid()); 
       return;
    }
    else             // 父进程打印进程信息,从结果看,可以正常运行
       printf("2:parent pid=%d,ppid=%d\n",getpid(),getppid());
}
void fun()           // 此函数代码,从结果看,可以正常运行
{                    // 但在父进程中没有能够运行,出现段错误 
   int i;
   int buf[100];
   for(i=0;i<100;i++)
       buf[i]=0;
   printf("3:child pid=%d,ppid=%d\n",getpid(),getppid());	
}
int main()
{
   pid_t pid;       // 给出临时变量,没有使用
   test();          // 调用test
   fun();           // 调用fun
}
下面是解释为什么出错:


在进程中运行新代码

函数功能及介绍

使用fork()函数创建子进程后,如果希望在当前进程中运行新的程序,可以调用execX系列函数。当进程调用execX

系列函数中的任意一个时,该进程用户资源完全由新程序替代。

函数区别:


具体各个函数的功能及使用查阅相关资料即可。

执行新代码对打开文件进行处理

在执行exec系列函数时,默认情况下,新代码可以使用原来的代码中打开的文件描述符,即执行exec系列函数时,并不关闭进程原来打开的文件。

回收用户空间资源

是linux系统下,可以使用下面的方式回收用户空间资源。

  • 显示调用exit或_exit系统调用
  • 在main函数中执行return语句
  • 隐含的离开main函数,例如遇到mian函数的}

进程在正常退出前都需要执行注册的退出处理函数,刷新流缓冲区等操作,然后释放进程用户空间所有资源。而进程控制块PCB并不在这时释放。仅仅调用退出函数的进程属于一个僵死进程。

C语言的exit和return有着本质的区别。

回收内核空间资源

上面介绍了进程退出时释放了用户空间的资源,但是,进程PCB么有释放,这一工作显然不是有自己完成的,而是由当前进程的父进程完成的,可显示的调用wait和waitpid函数来完成。具体函数结束,查阅函数说明文档即可。

孤儿进程和僵死进程

孤儿进程:因为父进程先退出导致一个子进程被init进程收养的进程成为孤儿进程,即是孤儿进程父进程改为init进程,内核资源也由其回收。

僵死进程:进程已经退出的,但是他的父进程还没有回收他的内核空间资源的进程。即该进程的在内核空间的PCB没有被回收。

下面是一个孤儿进程的示例:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
	pid_t pid;
	if((pid=fork())==-1)
		perror("fork");
	else if(pid==0)
	{
		printf("pid=%d,ppid=%d\n",getpid(),getppid());
		sleep(2);
		printf("pid=%d,ppid=%d\n",getpid(),getppid());
	}
	else
		exit(0);
}

1693是当期用户的init进程。

修改进程用户相关信息

1、access核实用户权限

此函数检查当前进程是否拥有对某文件的相应的访问权限。

2、设置进程真实用户UID

使用setuid函数。

setgid函数可以修改进程的用户的GID。

3、设置进程的有效用户EUID

使用seteuid函数

setegid设置EGID。

上面的函数只要知道就好,用的时候查看相应的文档说明就ok。

Linux的特殊进程

守候进程

守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程lpd等。
守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同Unix环境下守护进程的编程规则并不一致。这需要读者注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。

守候进程的启动方式:

  • 系统启动的时候由启动脚本启动。
  • 利用inetd超级服务器启动,如telnet。
  • 由cron命令定时启动以及在终端用nohup命令启动的进程也是守候进程。

 守护进程及其特性

守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。
总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。如果读者对进程有比较深入的认识就更容易理解和编程了。

 守护进程的编程要点

前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;
1. 在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续
2. 脱离控制终端,登录会话和进程组
有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid();
说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3. 禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork())
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4. 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
for(i=0;i 关闭打开的文件描述符close(i);>
5. 改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/")
6. 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

日志信息

为了告诉系统管理员守候进程的运行情况,特别是出现异常时,守候进程需要输出特定的信息,而守候进程又不能把信息输出到某个终端,因此一般采用输出到日志信息的方式。linux下面守候进程的写日志信息有两种方式:

  • 进程直接与日志文件建立联系

  • 使用日志守候进程

系统建立了日志守候进程syslogd专门管理各类日志文件。

一般情况下,调用openlog函数将于日志守候进程建立联系,就是需要写日志信息时,需要显示调用该函数。

syslog()将产生一条日志信息,然后由日志守候进程将其发布到各个日志文件中。

各个函数的具体用法需要时候参见说明。


守候进程示例:

#include <unistd.h> 
#include <signal.h> 
#include <fcntl.h>
#include <sys/syslog.h>
#include <sys/param.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <stdio.h>
#include <stdlib.h>

int init_daemon(const char *pname, int facility)
{ 
        int pid; 
        int i;
	     signal(SIGTTOU,SIG_IGN);  // 处理可能的终端信号
	     signal(SIGTTIN,SIG_IGN); 
	     signal(SIGTSTP,SIG_IGN); 
	     signal(SIGHUP ,SIG_IGN);
 
        if(pid=fork())            // 创建子进程,父进程退出
            exit(EXIT_SUCCESS); 
        else if(pid< 0) 
	  {
		perror("fork");
		exit(EXIT_FAILURE);
        }
        setsid();                // 设置新的会话组长,新进程组长,脱离终端
        if(pid=fork())           // 创建新进程,子进程不能在申请终端
                exit(EXIT_SUCCESS); 
        else if(pid< 0) 
	  {
		perror("fork");
		exit(EXIT_FAILURE);
        }  
        for(i=0;i< NOFILE;++i)           // 关闭父进程打开的文件描述符
                close(i);
         open("/dev/null", O_RDONLY);    // 对标准输入输出全部重定向到/dev/null
          open("/dev/null", O_RDWR);     // 因为之前关闭了所有文件描述符,新开的值位0,1,2
          open("/dev/null", O_RDWR);

        chdir("/tmp");                   // 修改主目录
        umask(0);                        // 重设文件掩码
        signal(SIGCHLD,SIG_IGN);         // 处理子进程退出
	  openlog(pname, LOG_PID, facility); // 与守候进程建立联系,加上进程号,文件名
	  return; 
} 

int main(int argc,char *argv[]) 
{ 
        FILE *fp; 
        time_t ticks; 
        init_daemon(argv[0],LOG_KERN); // 执行守候进程函数
        while(1)
        {
            sleep(1);
			ticks=time(NULL);  // 读取当前时间
            syslog(LOG_INFO,"%s",asctime(localtime(&ticks)));  // 写日志信息 
        }

} 


NEXT

进程间通信——管道

Linux异步信号处理机制


转载请注明出处:http://blog.csdn.net/suool/article/details/38419983,谢谢!