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 the
fork() function. Otherwise, -1 shall bereturned to the parent process, no child process shall be created, and
errno 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,谢谢!

Linux 程序设计学习笔记----进程管理与程序开发(下),布布扣,bubuko.com

时间: 2024-10-27 03:34:19

Linux 程序设计学习笔记----进程管理与程序开发(下)的相关文章

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

转载请注明出处,http://blog.csdn.net/suool/article/details/38406211,谢谢! Linux进程存储结构和进程结构 可执行文件结构 如下图: 可以看出,此ELF可执行文件存储时(没有调入内存)分为代码区.数据区和未出花数据区三部分. 代码区:存放cpu的执行的机器指令. 数据区:包含程序中的已经初始化的静态变量,以及已经初始化的全局变量. 未初始化数据区:存入的是未初始化的全局变量和未初始化的静态变量. 现在在上面的程序代码中增加一个int的静态变量

Linux 程序设计学习笔记----POSIX 文件及目录管理

转载请注明:http://blog.csdn.net/suool/article/details/38141047 问题引入 文件流和文件描述符的区别 上节讲到ANSI C 库函数的实现在用户态,流的相应资源也在用户空间,但无论如何实现最终都需要通过内核实现对文件的读写控制.因此fopen函数必然调用了对OS的系统调用.这一调用在LINUX下即为open, close, read, write等函数.这些都遵循POSIX标准. so,在linux系统中是如何通过POSIX标准实现对文件的操作和目

Linux 程序设计学习笔记----Linux下文件类型和属性管理

转载请注明出处:http://blog.csdn.net/suool/article/details/38318225 部分内容整理自网络,在此感谢各位大神. Linux文件类型和权限 数据表示 文件属性存储结构体Inode的成员变量i_mode存储着该文件的文件类型和权限信息.该变量为short int类型. 这个16位变量的各个位功能划分为: 第0-8位为权限位,为别对应拥有者(user),同组其他用户(group)和其他用户(other)的读R写W和执行X权限. 第9-11位是权限修饰位,

Linux 程序设计学习笔记----ANSI C 文件I/O管理

转载请注明出处:http://blog.csdn.net/suool/article/details/38129201 问题引入 文件的种类 根据数据存储的方式不同,可以将文件分为文本文件和二进制文件.具体的区别和关系如下: 文本文件与二进制文件在计算机文件系统中的物理存储都是二进制的,也就是在物理存储方面没有区别都是01码,这个没有异议,他们的区别主要在逻辑存储上,也就是编码上. 文本文件格式存储时是将值作为字符然后存入其字符编码的二进制,文本文件用'字符'作为单位来表示和存储数据,比如对于1

Linux程序设计学习笔记----System V进程通信(共享内存)

转载请注明出处:http://blog.csdn.net/suool/article/details/38515863 共享内存可以被描述成内存一个区域(段)的映射,这个区域可以被更多的进程所共享.这是IPC机制中最快的一种形式,因为它不需要中间环节,而是把信息直接从一个内存段映射到调用进程的地址空间. 一个段可以直接由一个进程创建,随后,可以有任意多的进程对其读和写.但是,一旦内存被共享之后,对共享内存的访问同步需要由其他 IPC 机制,例如信号量来实现.象所有的System V IPC 对象

Linux程序设计学习笔记----System V进程通信之消息队列

一个或多个进程可向消息队列写入消息,而一个或多个进程可从消息队列中读取消息,这种进程间通讯机制通常使用在客户/服务器模型中,客户向服务器发送请求消息,服务器读取消息并执行相应请求.在许多微内核结构的操作系统中,内核和各组件之间的基本通讯方式就是消息队列.例如,在 MINIX 操作系统中,内核.I/O 任务.服务器进程和用户进程之间就是通过消息队列实现通讯的. Linux中的消息可以被描述成在内核地址空间的一个内部链表,每一个消息队列由一个IPC的标识号唯一的标识.Linux 为系统中所有的消息队

Linux程序设计学习笔记----System V进程间通信(信号量)

关于System V Unix System V,是Unix操作系统众多版本中的一支.它最初由AT&T开发,在1983年第一次发布,因此也被称为AT&T System V.一共发行了4个System V的主要版本:版本1.2.3和4.System V Release 4,或者称为SVR4,是最成功的版本,成为一些UNIX共同特性的源头,例如"SysV 初始化脚本"(/etc/init.d),用来控制系统启动和关闭,System V Interface Definitio

Linux程序设计学习笔记----进程间通信——管道

转载请注明出处: http://blog.csdn.net/suool/article/details/38444149, 谢谢! 进程通信概述 在Linux系统中,进程是一个独立的资源管理单元,但是独立而不孤立,他们需要之间的通信,因此便需要一个进程间数据传递.异步.同步的机制,这个机制显然需要由OS来完成管理和维护.如下: 1.同一主机进程间数据交互机制:无名管道(PIPE),有名管道(FIFO),消息队列(Message Queue)和共享内存(Share Memory).无名管道多用于亲

Linux 程序设计学习笔记----命令行参数处理

转载请注明出处.http://blog.csdn.net/suool/article/details/38089001 问题引入----命令行参数及解析 在使用linux时,与windows最大的不同应该就是经常使用命令行来解决大多数问题.比如下面这样的: 而显然我们知道C语言程序的入口是mian函数,即是从main函数开始执行,而main函数的原型是: int main( int argc, char *argv[] ); int main( int argc, char **argv );