Linux的进程编程-之一-基本概念

1    进程的基本概念

1.1         进程状态和状态转换

进程在Liunx系统中的各种状态:

1.用户状态     :进程在用户状态下的运行状态。

2.内核状态     :进程在内核状态下的运行状态。

3.内存中就绪 :只要内核调度它,就可以执行。

4.内存中睡眠 :

5.内存外就绪 :进程处于就绪状态,但是必须把它放入内存,内核才能调度它执行。

6.内存外睡眠 :

7.被抢先         :进程从内核态返回用户态时,内核调度了另一个进程,则原进程被抢先。

8.创建状态     :刚创建的进程既不是就绪状态,也不是睡眠状态。

创建状态是除了进程0以外所有进程的初始状态。

9.僵死状态(zombie):进程调用exit( )结束,但在进程表项中仍有纪录,并可由父进程收集。

 

父进程通过调用fork( )来创建子进程,调用fork()时,子进程首先处于创建态,fork( )调用为子进程配置好内核数据结构和子进程私有数据结构后,子进程就要进入(内存中或内存外)就绪态。Linux是分时系统,内核给每个进程分一个时间片,当前进程的时间片用完就会调度另一个进程执行。如果子进程在内存中就绪,就可以被内核调度执行。

如果进程请求的资源得不到满足,就会进入睡眠状态,直到它请求的资源得到满足,才会被内核唤醒而进入就绪态。如果进程在内存中睡眠时,内存不足,当睡眠时间达到一个值,该进程就会被移出内存,在SWAP设备上睡眠。同样如果进程在内存中就绪时,内存不足,当就绪时间达到一个值,该进程就会被移出内存,在SWAP设备上就绪。

当进程要申请多种共享资源时,Linux保证操作的原子性,也就是说进程要么申请到所有资源,要么放弃所有资源,这样能够保证不会造成多个进程之间形成互锁。

1.2         进程控制

Linux系统的启动过程:

1.计算机先通过硬件将引导块的内容读到内存并执行;

2.引导块将内核从文件系统中装入内存,并将控制转交给内核,内核开始运行;

3.内核首先初始化它的数据结构,并将根文件系统安装到“/”,为进程0形成执行环境;

4.设置好进程0的环境后,内核便作为进程0开始执行,并调用fork()。因为这时进程0运行在内核状态,所以新的进程(进程1)也运行在内核状态;

5.进程1创建并保存好用户寄存器上下文。然后进程1就从内核状态返回用户状态执行从内核拷贝的代码,并调用exec执行/sbin/init,进程1通常称为初始化进程,它负责初始化新的进程。

1.2.1       fork( )

#include<unistd.h>

#include<sys/types.h>

pid_tfork( void )

调用成功时,对父进程返回子进程的PID,对子进程返回0。

调用失败时,对父进程返回-1,不会创建子进程。

fork( )在内核中要进行以下操作:

第一步,为新进程在进程表中分配一个表项,并复制父进程的进程表项给子进程;

第二步,分配给子进程一个唯一的进程标识号(PID),其实就是进程表中对应表项的索引号;

进程表项包括:进程的UID和GID、打开文件的描述符、环境变量、信号控制设定、nice值、进程调度类别、当前工作目录、根目录(根目录不一定是“/”,它可由chroot()函数改变)、资源限制、控制终端等。

子进程与父进程不同的有:进程号(PID),父进程号(PPID),子进程中pending的信号集被初始化为空,子进程不继承父进程中的timer设置。

1.2.2       vfork( )

#include<unistd.h>

#include<sys/types.h>

pid_tvfork( void )

当fork( )创建子进程之后,父进程不会阻塞,父进程和子进程都会继续执行fork( )调用之后的指令,而且fork()会进行进程地址空间的拷贝,也就是说子进程将获得父进程数据段、堆和栈的副本。由于这些都是副本,父子进程并不共享这部分内存,所以子进程对父进程中的同名变量进行的修改并不会影响其在父进程中的值。但是父子进程共享代码段,因为代码段通常是read-only的。当然,用fork()创建子进程后,子进程通常要调用exec( )执行另一个程序。当进程调用exec()后,进程由新程序的main函数开始执行,并且还会根据新程序创建新的代码段、数据段、堆和栈。

当vfork( )创建子进程之后,父进程被暂时阻塞,直到子进程调用exec( )或_exit( )之后,父进程才可以继续运行,所以vfork( )可以保证子进程优先运行。

此外,vfork( )并不进行进程地址空间的拷贝,也就是说vfork( )出来的子进程是在父进程的地址空间中运行的,直到子进程调用exec( )执行另一个程序,此时子进程会根据新程序创建新的代码段、数据段、堆和栈。所以由vfork( )创建的子进程必须小心,以免改变父进程的数据。

所以从实际使用上讲,调用vfork( )创建子进程,只是为了在子进程中调用exec( )。而且,由于现在的fork( )采用了“写操作时拷贝”(copy-on-write),该机制伪装对进程地址空间的真实拷贝,所以父子进程在刚开始运行时,实际上访问的是相同的内存空间,直到有任何一个进程改变内存中的数据时,才进行进程地址空间的拷贝。这在提高很大程度上抹杀了使用vfork( )的理由,事实上,大部份系统完全丧失了vfork( )的功能,但为了兼容,它们仍然提供vfork( )调用,但只是简单地调用fork()。

1.2.3       exit( )

#include<stdlib.h>

voidexit( int status )

参数status作为退出的状态值返回父进程,该值可以用wait( )来收集。返回状态码status只有最低一个字节有效。

进程发出exit( )调用后,内核会释放该进程所占用的资源,并会保留进程表项,将进程表项中的进程状态设为僵死状态,并对该进程的父进程发出一个SIGCHLD信号。

1.2.4       _exit( )

#include<unistd.h>

void_exit( int status )

_exit直接由内核接管进行清理,而exit()先进行用户空间的清理,包括调用atexit( )登记的程序终止函数,执行I/O的标准清理程序(关闭所有I/O流,关闭所有打开的文件,写出任何缓冲输出等),然后才由内核接管进行清理。在main函数中,exit(0)等价于return 0。

在由fork( )创建的子进程中,如果没有调用exec(),应该使用_exit( )退出。因为如果父子进程都用exit()退出的化,可能会导致用户空间清理程序的重复执行。还有一些特殊情况,比如daemon程序,需要父进程调用_exit(),而不是子进程。在由vfork( )创建的子进程中,exit()更加危险,因为exit( )可能会影响父进程的状态。

基本规则是,每进入一次main函数,exit( )只能调用一次。

exit( ) terminate the calling process withthe following consequences:

1.       All ofthe file descriptors open in the calling process are closed.

2.       If theparent process of the calling process has neither set SA_NOCLDWAIT flag nor setSIGCHLD to SIG_IGN:

a)       If theparent process of the calling process is executing wait( ) or waitpid( ), it isnotified of the calling process' termination and the low-order 8 bits of the statusare made available to it. If the parent is not waiting currently, the statuswill be available when it subsequently executes wait( ) or waitpid( ).

b)       If theparent process of the calling process is not executing wait( ) or waitpid( ),the calling process is transformed into zombie process. A zombie process is aninactive process and it will be deleted at some later time when its parentprocess executes wait( ) or waitpid( ).

3.       SIGCHLDwill be sent to the parent process. If the parent process of the callingprocess has set SA_NOCLDWAIT flag, it is implementation-dependent whetherSIGCHLD signal will be sent to the parent process.

4.       If theparent process has set SA_NOCLDWAIT flag, or set SIGCHLD to SIG_IGN, the statuswill be discarded, and the calling process will end immediately.

5.       Theparent process ID of all the calling process' existing child processes andzombie processes is set to the process ID of an implementation-dependent systemprocess. That is, these processes are inherited by a special system process(init process in Linux).

6.       If theprocess is a controlling process, the SIGHUP signal will be sent to eachprocess in the foreground process group of the controlling terminal belongingto the calling process.

7.       If theprocess is a controlling process, the controlling terminal associated with thesession is disassociated from the session, allowing it to be acquired by a newcontrolling process.

8.       If theexit of the calling process causes a process group to become orphaned, and ifany member of the newly-orphaned process group is stopped, a SIGHUP signalfollowed by a SIGCONT signal will be sent to each process in the newly-orphanedprocess group.

1.2.5       atexit( )

#include<stdlib.h>

intatexit( void (*function)(void) )

typedefvoid (*ExitFUN)(void)

intatexit( ExitFUN fun )

当程序通过exit( )或从main中return时,function指定的函数会先被调用,然后才真正由exit( )结束程序。一个进程最对可登记32个终止处理函数, 这些函数按登记相反的顺序自动调用。

成功返回0,失败返回-1。

1.2.6       exec( )

#include<unistd.h>

externchar **environ

intexecl      ( const char * path,   const char *arg0, ... )

intexecle     ( const char * path,   const char *arg0, ... , char* const envp[ ] )

intexecv     ( const char * path,   char *const argv[ ] )

intexecve    ( const char * path,   char *const argv [ ], char *const envp[ ] )

intexeclp    ( const char * file, const char*arg0, ... )

intexecvp   ( const char * file, char *constargv[ ] )

参数path包含执行文件的路径和文件名,而参数file可以不用包含执行文件的路径,而通过环境变量PATH指定的路径进行搜索。

参数arg0argv[0]指定的是ps显示的程序名;arg0,...的最后一个参数和argv[]的最后一个成员都必须是(char*)0或NULL。

envp[]中的每一个成员做为环境变量传入执行程序,envp[]中每一个成员的格式必须为“key=value”,而且envp[ ]的最后一个成员也必须是(char*)0或NULL。

需要注意的是,exec( )如果调用成功,是不会返回的,而是直接转到新程序中运行了。所以如果exec( )有返回,也就是说exec()之后的语句能够被执行,说明exec( )肯定失败了,返回-1。

1.2.7       wait( )

#include<sys/types.h>

#include<sys/wait.h>

pid_twait( int *status )

调用wait( )的进程将被挂起,直到它的一个子进程退出或者收到一个不能被忽略的信号。如果wait( )调用发生时,已经有退出的子进程(该子进程处于僵死状态),则wait( )调用立即返回,其中参数status中包含子进程退出时的返回状态码。

成功返回子进程的PID,失败返回-1。

1.2.8       waitpid( )

#include<sys/types.h>

#include<sys/wait.h>

pid_twaitpid( pid_t pid, int *status, int options )

参数pid的的取值及含义:

pid = 0  :等待和当前进程GID相同的任一子进程退出

pid<-1  :等待任一进程GID等于pid绝对值的子进程退出

pid = -1       :等待任何子进程退出,相当于调用wait( )。

参数options的取值及含义:

WNOHANG     :如果没有子进程退出就立即返回。

1.2.9       sleep( )

#include<unistd.h>

unsignedint sleep(unsigned int seconds)

将进程挂起指定的秒数,除非其间收到不可忽略或能使进程终止的信号。

如果指定的挂起时间到了,返回0;如果被信号打断,则返回剩余的时间。

1.2.10   usleep( )

#include<unistd.h>

intusleep( useconds_t useconds );

将进程挂起指定的毫秒数,除非其间收到不可忽略或能使进程终止的信号。

如果指定的挂起时间到了,返回0;如果被信号打断,则返回-1。

1.2.11   nanosleep( )

#include<time.h>

intnanosleep( const struct timespec *requestTP, struct timespec *remainTP );

struct timespec

{

time_t    tv_sec           //seconds

long       tv_nsec         //nanoseconds

}

将进程挂起参数requestTP指定的时间,除非其间收到不可忽略或能使进程终止的信号。

如果指定的挂起时间到了,返回0;

如果被信号打断,则返回-1,参数remainTP返回剩余的时间。

1.3         Daemon进程

1.3.1       相关概念-进程组、会话、控制终端

1.3.1.1     进程组

Shell的一条命令行形成一个进程组;

每个进程属于一个进程组;

每个进程组有一个group leader;

进程只能将自身和其子进程设置为进程组id;

当子进程调用exec( )函数之后,就不能再将该子进程设置为进程组id。

 

#include<unistd.h>

pid_tgetpgid( pid_t pid )

获得pid进程所在的进程组id;如果pid=0,表示当前进程。失败返回-1。

intsetpgid( pid_t pid, pid_t pgid )

将pid进程加入pgid进程组,如果pgid进程组不存在,则首先创建pgid进程组。

如果pid=0,则改变当前进程的进程组;如果pgid=0,相当于pgid=当前进程id。

注意:不能改变session leader的进程组id。

成功返回0,失败返回-1。

 

#include<unistd.h>

pid_tgetpgrp( void )

获得当前进程的进程组id。

intsetpgrp( void )

设置当前进程id为进程组id。

注意:setpgrp( )对sessionleader不起任何作用。

1.3.1.2     会话

一次登录形成一个会话。

一个会话可包含多个进程组,但只能有一个前台进程组,其余的都是后台进程组。

 

#include<unistd.h>

pid_t getsid( pid_t pid )

获得pid进程所在会话的session leader的进程组id;如果pid=0,表示当前进程。失败返回-1。

pid_t setsid( void )

只有当前进程不是group leader时,才会创建新的会话。如果新会话创建成功:

1.当前进程会成为新会话的session leader;

2.当前进程会成为新进程组的group leader,新进程组的id会被设置为当前进程的id;

3.新会话没有控制终端;

创建成功返回新进程组的id;失败返回-1。

1.3.1.3     控制终端

一个会话只能有一个或没有控制终端;

session leader打开一个终端之后,该终端就成为会话的控制终端;

控制终端上产生的输入和信号,将会发送给会话前台进程组中的所有进程;

1.3.2       Daemon进程的创建

以daemon方式运行的程序没有控制终端,在运行过程中与终端无关,即不受终端操作的影响,在终端退出后也能继续运行,而其它非Daemon程序受终端操作的影响,而且会在终端退出时退出。

成为daemon进程的步骤:

1.       fork( )之后父进程退出,用来保证子进程不是group leader,才能成功调用setsid();

2.       子进程调用setsid( );

3.       在二次fork( )之前显式忽略SIGHUP信号,这是因为当sessionleader结束时,系统会向session中的前台进程组中的每一个进程发送SIGHUP信号;

4.       二次fork( )之后父进程退出,用来保证子进程不是session leader,永远不会重获控制终端;

5.       创建daemon进程的pid_file。当需要在程序的某个地方终止daemon进程时,可以读取pid,然后再调用kill(pid, 9 )或者system("kill -s 9 pid")杀死daemon进程;如果没有记录daemon进程的pid,就需要在“/proc/”目录中查找daemon进程的pid,然后再杀死它;

6.       调用chdir( );

7.       调用umask( ),更改继承的mask,这一步是可选的,因为daemon程序不一定读写文件;

8.       调用close( )关闭文件描述符0,1和2,释放从父进程继承的stdin、stdout、stderr,而且需要关闭所有可能打开的文件描述符;

9.       出于安全性考虑,即使daemon进程不使用stdin、stdout、stderr,也应重定向三者到/dev/null。

如果session leader调用open( )打开一个(伪)终端时没有指定O_NOCTTY,而且该终端不是其它session的控制终端,那么该终端成为session leader的控制终端。

1.3.3       实例程序

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <fcntl.h>

#include <signal.h>

#include <stdarg.h>

#include <sys/types.h>

#include <sys/stat.h>

int main( )

{

// ……

if ( make_daemon( ) )

{

// do what we want to do asdaemon;

return 0;

}

printf(" Error : Can notcreate the daemon process ! \n");

return 0;

}

/*****  make_daemon( )使当前进程成为daemon进程   *************/

int make_daemon ( const char *workdir = "/",mode_t mask = 0 )

{

int pid;

 

// 1. create first child, and terminateparent

if( ( pid = fork( ) ) < 0 )

return -1;      // fork failed, so simply return to parent’smain( )

else if( pid != 0 )

_exit(0);        // fork success and we are in the parent process. so simplyexit

 

// 2. create new session

// set current process ( first child) to be session leader and process group leader

if ( setsid( ) = = -1 )

{

return -1;      // setsid( ) failed, so simply return to firstchild’s main( )

}

 

// 3. ignore SIGHUP sent bysystem when first child (session leader) exit

signal(SIGHUP, SIG_IGN );

/***********************************************************

signal(SIGTTIN,SIG_IGN);

signal(SIGTTOU,SIG_IGN);

signal(SIGTSTP,SIG_IGN);

signal(SIGCHLD, SIG_IGN);

/***********************************************************

 

// 4. create second child, andterminate first child

if( ( pid = fork( ) ) < 0 )

return -1;      // fork failed, so simply return to firstchild’s main( )

else if( pid != 0 )

_exit(0);        // fork success and we are in the first child process. sosimply exit

 

// 5. create pid_file to recordthe daemon process’s pid

if (create_pid_file( ) = = -1 )

{

return -1;      // create pid_file failed, so simply returnto second child’s main( )

}

 

// 6. change working directory

chdir( workdir );

 

// 7. clear file mode creationmask

umask( mask );

 

// 8. close file descriptors

int maxFdNum;                              //  long int maxFdNum;

maxFdNum = getdtablesize( );       // maxFdNum = sysconf( _SC_OPEN_MAX );

 

for ( int fd = 3; fd < maxFdNum;fd++ )

close( fd );

// ***********************************************************

struct stat fstatus;

for ( int fd = 3; fd < maxFdNum;fd++ )

{

if ( fstat( fd, &fstatus ) == 0 )

close( fd );

}

**************************************************************//

 

// 9.re-direct stdin, stdout, stderr

int null_fd;

null_fd = open("/dev/null", O_RDWR );

if ( null_fd < 0 )

return -1;

dup2( null_fd, 0 );

dup2( null_fd, 1 );

dup2( null_fd, 2 );

 

return 1;

}

 

 

/*****  create_pid_file( )创建daemon的pid_file   *************/

int create_pid_file( )

{

int pid_fd =open("path+pid_filename",O_RDWR | O_EXCL | O_CREAT );

if ( pid_fd < 0 )

return -1;

 

int ret = fchmod(pid_fd,S_IRWXU|S_IRWXG|S_IRWXO);

if ( ret < 0 )

return -1;

 

FILE *pid_file = fdopen( pid_fd,"w" );

if ( pid_file == NULL )

return -1;

fprintf( pid_file,"%d", getpid( ) );

 

fclose(pid_file);

close(pid_fd);

 

return 1;

}

1.4         去除僵死进程

当子进程比父进程先结束,内核仍然保存一些子进程的信息,以便父进程会需要它。为了得到这些信息,父进程调用wait( ),当这个调用发生,内核可以丢弃子进程的这些信息。从子进程终止到父进程调用wait( )的时间里,子进程称为僵死(zombie)进程。即使它没有在执行,它仍然占据进程表里一个位置。

如果父进程未调用wait( )而终止,子进程将被init进程接管,由init进程控制子进程退出后的清除工作。

为了避免僵死(zombie)进程的出现,需要确认父进程为每个子进程调用wait( )或waitpid( );或者调用signal( SIGCLD, SIG_IGN )告诉系统对子进程的退出没有兴趣,系统将不产生僵死进程。如果signal设置成功,那么wait()或waitpid( )将不再正常工作;如果任何一个被调用,将等待直到所有子进程退出,然后返回失败。

另一种方法是二次fork( ),并使第一个fork出来的子进程直接退出,造成第二个fork出来的子进程变成孤儿(orphaned)进程,init进程将负责清除它。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值