进程基本知识

进程环境及进程属性:
        进程资源由两部分组成:内核空间进程资源及用户空间资源。内核空间资源即PCB相关的信息,包括进程控制块本身,打开的文件表项、当前目录、当前终端信息,线程基本信息、可访问内存地址空间。PID、PPID、UID、EUID等。也就是说通过PCB可访问该进程所有的资源。用户空间的进程资源包括:通过成员mm_struct映射的内存空间。

        用户及进程拥有以下几种状态:就绪、运行、等待(可被中断和不可被中断),停止状态和僵死状态。内核进程状态与用户进程略有差异。

进程组号:
        每个进程都有一个唯一的进程组号;进程组是一个或多个进程的集合,它们与同一作业相关联,可以接受来自同一终端的各种信号。每个进程组都有唯一的进程组号,进程组号可以在用户层修改。
        每个进程组都可以有一个组长进程,组长进程的进程组号等于其进程号。但组长进程可以先退出,即只要在某个进程组中有一个进程存在,则该进程组就存在,与其组长进程是否终止无关。

        getpgid()获得指定进程的进程组号,setpgid()函数用来将某个进程加入到某个进程组。一个进程只能为自己或子进程设置进程组号PGID,在它的子进程调用了exec函数后,就不能再改变该子进程的进程组号了。

会话:
        会话是一个或多个进程组的集合。
        系统调用函数setsid()来获取某个进程的会话号SID,
        extern _pid_t getsid (__pid_t __pid)
        如果pid 是0,则返回调用进程的会话号SID,一般来说该值等于进程组号PGID。如果pid并不属于调用者所在的会话,则调用就不能获得SID。(即pid属于调用者所在的会话,这样获取自己的会话与获取别人的会话是一样的SID)。某进程的会话号是可以修改的,使用setsid()函数修改。
setsid()函数、进程组与会话的关系:
如果调用进程已经是一个进程组的组长,则此函数会创建一个新的会话。
1、该进程变成会话首进程(session leader),会话首进程是创建该会话的进程。
2、该进程成为一个新进程组的组长进程,新进程组PGID是该调用进程的PID。

3、该进程没有控制终端,如果在调用setsid之前该进程就有一个控制终端,那么这种联系也会被中断。

控制终端:
会话和进程组有以下一些特点:
1、一个会话可以有一个控制终端,建立与控制终端连接的会话首进程被称为控制终端进程。
2、一个会话中的几个进程组可以被分成一个前台进程组和几个后台进程组,如果一个会话有一个控制终端,则它有一个前台进程组。
3、终端的中断(delete 或ctrl + c)或终端的退出(ctrl + \)都发送给前台进程组的所有进程。

终端与进程关系的相关函数:
tcgetpgrpt():获得当前前台进程组的进程组号。
pid_t tcgetpgrp(int filedes):返回与打开的终端(由filedes 指定)相关前台进程的进程组号。
pid_t tcsetpgrp(int filedes,pid_t pgrpid):如果进程有一个控制终端,则将前台进程组ID设置为pgrpid。pgrpid 的值应该在同一会话中的一个进程组的ID,filedes为控制终端的文件描述符。

tcgetsid():可以获取控制终端的会话首进程的会话ID pid_t tcgetsid(int filedes)。

进程用户属性:
        进程的真实用户号RUID,可用getuid()函数获得用户ID(执行当前进程的用户的用户ID)。
        进程有效用户号EUID,EUID主要用于权限检查,一般情况下EUID与UID相同,但如果可执行文件的setuid位有效,该文件的拥有者之外的那些有效用户组EUID为该文件的拥有者,也可执行此可执行文件。使用方法:chmod u+s 文件名,文件的读写执行权限中增加了s项。即设置了setuid位的可执行文件,其他用户在执行该文件时具有该文件拥有着对该文件的访问权限,当前用户的EUID为该可执行文件的拥有着的ID值,EUID不再与UID相同。
geteuid()函数:获得执行当前进程的用户的EUID。这个EUID是可执行文件的拥有者的用户ID,是文件拥有者的用户ID。
进程用户组号GID:创建进程的用户所在的组号为该进程的进程用户组号。
有效进程用户组号:EGID,一般情况下EGID与GID相同。但某可执行文件设置了setgid位,那么任何用户运行此程序时,其有效用户组号EGID为该文件的拥有者所在的组。

进程管理及控制:
创建进程:
        fork函数成功后,子进程从父进程继承下列属性:有效用户/组号、进程组号、环境变量、对文件的执行时关闭标志、信号处理方式设置、信号屏蔽集合、当前工作目录、文件模式掩码、文件大小限制,打开的文件描述符(共用同一个文件表项)。子进程的执行位置为fork的返回位置,fork之前的代码无法执行。子进程的用户空间复制父进程的用户空间的所有信息。
子进程对父进程文件流缓冲区的处理:
        父进程流缓冲中有内容(暂时没有回车输出),此流缓冲的内容也复制到子进程中。子进程的流也有 没输出的内容了。如:父进程中的流内容:printf("before fork,no enter:pid =%d\t",getpid());(没有回车),则父、子进程都会输出上面的内容,且pid的值都是父进程的pid值。
子进程对父亲进程打开的文件描述符的处理:
        子进程复制父亲进程的数据段,BSS段、代码段、堆空间、栈空间和文件描述符,而对文件描述符关联的内核文件表项,则是采用共享的方式。
父进程A:文件描述符1 2 3 4 .....

子进程B:文件描述符1 2 3 4 .....  ,共享文件读写位置。

vfork:
fork子进程中复制父进程栈空间、子进程拥有独立的栈空间。

vfork,共享代码段和数据段,但什么时候真的复制?

vfork函数测试:

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

void test(void)
{
	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 0;	
	}
	
	else 
		printf("2:parent pid =%d,ppid=%d\n",getpid(),getppid());
}

void fun(void)
{
	int i;
	int buf[100];	
	for(i=0;i<100;i++)
		buf[i]=0;
	
	printf("3:child pid =%d,ppid=%d\n",getpid());
}

int main(void)
{
	pid_t pid;
	test();
	fun();	
}

编译运行结果:
1:child pid = 2872,ppid = 2871
3:child pid = 2872,ppid=2871
2:perent pid = 2871,ppid=2806
Segmentation fault
解释:
1、调用main函数,申请栈空间,没有问题。
2、调用test函数申请栈空间,此程序调用vfork函数,因为vfork创建的子进程共享父进程的栈空间,而子进程先执行,因
此得以正常运行,然后继续执行后返回,将清理栈空间。
3、子进程再调用fun函数时,将覆盖原来test函数栈空间,继续执行fun函数,没有出现异常。

4、子进程退出后,父亲进程从vfork的返回处执行代码,没有问题,但返回时该栈空间已经不存在了,因此出现栈错误。

在进程中运行新的代码:
        fork后在子进程中运行新的程序,则可调用execX系列函数。
        execX系列函数的区别是:指示新的程序的位置是使用路径还是文件名,如果使用文件名,则在系统得$PATH环境变量所描
述的路径中搜索该程序。在使用参数时是使用参数列表的方式还是使用argv[]数组的方式。
        execl /execlp/execle/execv/execvp/execve
p:使用文件名,环境变量描述的路径中搜索;没P:使用绝对路径
l:参数列表;
v:使用argv[];
    
    execl:
    原型:extern int execl (__const char *__path,__const char *__arg,...)
    execl()用来执行参数path字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须
是空指针以标识参数列表为空。如:execl("/bin/ls","ls","-l","/home",(char *)0);    ls也作为程序参数传入。
    
    execle:
    原型:extern int execle (__const char *__path,__const char *__arg,...)
    execle()用来执行参数path字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须指
向一个新的环境变量数组,即新执行程序的环境变量。如:execle("/bin/ls","ls","-l","/home",(char *)0,env);
    
    execlp:
    原型:extern int exexlp(__const char *__file,__const char *__arg,...)
    execlp()会从$PATH环境变量所指的目录中查找文件名为第一个参数指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须用空指针NULL。如:execlp("ls","ls","-l","/home",(char *)0);

    execv:
    原型:extern int execv (__const char *__path,char *__const __argv[])
    execv 用来执行参数path字符串所指向的程序,第二个参数为数组指针维护的程序参数列表,该数组的最后一个成员必须为NULL。如:char *argv[]={"ls","-l","/home",(char *)0};   execv("/bin/ls",argv);

    execvp:
    原型:extern int execvp(__const char *__file,char *__const __argv[])
    execvp()会从$PATH环境变量所指的目录中查找文件名为第一个参数指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个成员必须为NULL。如:char *argv[]={"ls","-l","/home",0};execvp("ls",argv);
    

    除以上以外,system()以新进程方式运行一个程序,然后结束,system()函数用来创建新进程,并在此进程中运行新进程,直到新的进程结束后,才继续运行父进程。

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

        fork 出来的子进程共享文件表项,文件描述符也与父进程对应、一样,在子进程中execX 新代码共享文件表项,共享文件读写位置。默认情况下,新代码执行execX后,对原打开的文件处理后,并不关闭原打开的文件。除非在execX中调用fcntl(fd,F_SETF,FD_CLOEXEC),即关闭FD_CLOEXEC项,则在执行execX后将关闭原来打开的文件描述符。

回收进程用户空间资源:
        Linux系统下,可以通过以下方式结束进程:
1、显式的调用exit或_exit系统调用。
2、在mian函数中执行return 语句。
3、隐含的离开main函数,例如遇到main的"}"。
进程在正常退出前都需要执行注册的退出处理函数,刷新流缓冲区等操作,然后释放进程用户空间所有资源。而进程控制块PCB并不在这时释放,仅调用退出函数的进程属于一个僵死进程。

exit与return的区别:
        exit用于退出进程。在正式释放资源前,将以反序的方式执行由on_exit() and atexit()函数注册的清理函数,同时刷新流缓冲区。
extern void exit(int __status) :如果成功,没有返回值,把参数status(标识退出状态)返回个父进程。否则返回-1。
        return退出当前函数,exit()退出当前进程。在main中return与exit完成一样的功能。调用exit要调用一段进程终止处理程序,然后关闭所有的I/O流。

_exit函数直接退出:
        _exit函数不调用任何注册函数而直接退出进程。 extern void _exit(int __status);_exit仅把参数status返回个父进程而直接退出,此函数调用后不会返回,而是传递SIGCHLD信号给父进程,父进程可以通过wait函数获得子进程的结束状态,_eixt()不会处理标准I/O缓冲区,如果需要则调用exit()。

注册退出处理函数:
atexit()和on_exit()用来注册在执行exit()函数前执行的操作函数,其实现使用了回调函数的方法:
extern int atexit (void (*__func) (void))
extern int on_exit(void (*__func) (int __status,void *arg),void *__arg);
两个函数的功能都是告诉进程,在正常退出是执行注册的func函数。两者的区别是func有参数和无参数。
对有参数func(),第一个参数为退出的状态,在执行exit()函数第此参数值为exit()函数的参数。第二个参数为用户输入的信息,一个无类型的指针,用户可以指定一段代码位置或输出 信息。
如:main()函数: 
    char *str = "test";
    on_exit(test_exit,(void *)str);
test_exit()函数:
     test_exit(int status,void *arg)
    {
        printf("arg = %s\n",(char *)arg);

    }

回收内核空间资源:
进程PCB资源的释放,可由父进程显式地调用wait()和waitpid()函数来完成。
1、wait()函数等待子进程结束,阻塞式等待该进程的任意一个子进程结束。回收该子进程的内核进程资源。
    extern __pid_t wait(__WAIT_STATUS __stat_loc),等待到任意一个子进程结束,将返回当前结束的子进程的PID,同时将子进程退出时的状态存储在"__stat_loc"变量中。
2、waitpid等待指定子进程结束,更加灵活地指定等待那个进程结束。
//come from /usr/includel/sys/Wait.h
extern __pid_t waitpid (__pid_t __pid,int *__stat_loc,int __options)
第一个参数位pid取值范围,指定了等待的是什么进程:
pid>0:表示等待进程pid位该pid值的进程结束。
pid=-1:表示等待任意子进程结束,相当于调用wait函数。
pid=0:表示等待与当前进程的进程组PGID一致的进程结束。

pid<-1:表示等待进程组PGID是此值的绝对值的进程结束。

孤儿进程与僵尸进程:
孤儿进程:因父进程先退出而导致一个子进程被init进程收养的进程为孤儿进程。即孤儿进程的父进程改为init进程。孤儿进程的内核空间资源将由init进程回收。

僵死进程:进程已经退出,但它的父进程还没有回收内核资源的进程为僵死进程,即该进程在内核空间德 PCB没有释放。

修改进程用户相关信息:
        access()核实用户权限:
此函数用来检查当前进程是否拥有对某个文件的相应访问权限。此函数定义如下:
extern int access(__const char *__name,int __type);
此函数的第一个参数为欲访问的文件(需包含路径),第二参数为相应的访问权限,可取值0(F_OK)1(X_OK) 2(W_OK) 4(R_OK),句有所测试的权限,则返回0,否则返回-1.

设置当前进程真实用户RUID函数:setuid()
如果当前用户是超级用户,则将设置当前进程的真实用户RUID为setuid()指定的ID;

如果当前用户是普通用户,且欲设置的UID为自己的UID,则可以,否则无权设置当前进程RUID。

守候进程:
        守候进程(Daemon)是在后台运行的一种特殊进程,他脱离于终端,从而这可避免进程被任何终端所产生的信号所打断,他在执行过程中的产生信息也不在任何终端上显示。守候进程周期性的执行某种任务或等待处理某些发生的事件,Linux的大多数服务器就是用守候进程实现的,比如web服务器HTTPd等。
一般情况下,守候进程可以通过以下方式启动:
1、在系统启动时由启动脚本启动,这些启动脚本通常放在/etc/rc.d目录下。
2、利用inetd超级服务器启动,如telnet等。
3、由cron命令定时启动以及在终端用nohup命令启动的进程也是守护进程。
守护进程编程要点:
1、屏蔽信号。
2、后台运行。
3、脱离控制终端和进程组。
4、禁止进程重新打开控制终端。
5、关闭打开的文件描述符。
6、改变当前工作目录。
7、重设文件创建掩码。

8、处理SIGCHD信号(子进程退出信号。)

日志信息:
为了告诉系统管理员守候进程的运行状况,特别是出现异常时,守候进程需要输出特定的信息,而守候进程又不能把信息输出到某个终端,守候进程一般采用日志信息的方式输出,在Linux系统下,守候进程有两种写日志的方式。
1、进程直接与日志文件建立联系(或自己创建一个独立的日志文件),即open该文件,然后调用write函数写日志。
2、为了便于管理日志,系统创建了日志守候进程syslogdz专门负责管理日志文件。因此,要向日志文件中写日志信息,只需要将日志发送给日志守候进程,在他的子进程调用了exec函数后,就不能再改变该子进程的进程组号了。

守候进程与日志示例:

#include<unistd.h>
#include<signal.h>
#include<fcntl.h>
#include<sys/syslog.h>
#include<sys/param.h>
#include<sys/stat.h>
#include<sys/types.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(&tics)));	
	}
}
测试Daemon:
gcc Daemon_exp.c -o Daemon_exp
./Daemon_exp 
ps aux|grep Daemon_exp
book      1847  0.0  0.0   1656   460 ?        S    08:45   0:00 ./Daemon_exp
book      1858  0.0  0.0   3036   792 pts/0    R+   08:45   0:00 grep --color=auto Daemon_exp
tail /var/log/messages //查看写入的信息。

更详细的内容请查看杨宗德老师的原版书籍:《Linux高级程序设计》。

猜你喜欢

转载自blog.csdn.net/qq_22863733/article/details/80215390