Linux环境编程之进程(四):创建新进程、执行程序和进程终止

引言:

对于每个进程,都有一个非负整数表示的唯一进程ID。虽然进程的ID是唯一的,但却是可重用的。系统中有一些专用的进程。如ID为0的进程通常是调度进程,也成交换进程或系统进程(它是内核进程)。进程ID为1通常是init进程,它是一个普通的用户进程。一些与进程ID有关的函数:

#include <unistd.h>

pid_t getpid(void);   //返回值:调用进程的进程ID

pit_t getppid(void); //返回值:调用进程的父进程ID

uid_t getuid(void); //返回值:调用进程的实际用户ID

uid_t geteuid(void); //返回值:调用进程的有效组ID

gid_t getgid(void); //返回值:调用进程的有效用户ID

git_t getegid(void); //返回值:调用进程的有效组ID

(一)

一个现有进程可以通过调用fork函数创建一个新的进程。由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新的子进程的进程ID。子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父子进程并不共享这些存储空间部分。父子进程共享正文段。由于在fork之后,经常跟随者exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用写时复制技术。

fork的一般使用形式如下:

if((pid = fork()) < 0){ // 用fork创建新进程
	printf("fork error");
}else if(pid == 0){
	//子进程的操作
}else{
	//父进程的操作
}

父、子进程之间的区别是:

fork的返回值不同;进程ID不同;两个进程具有不同的父进程ID:子进程的父进程ID是创建它的进程的ID,而父进程的父进程ID则不变;子进程的tms_utime、tms_stime、tms_cutime和tms_ustime均被设置为0。父进程设置的文件锁不会被子进程继承。子进程的未处理的闹钟被清除。子进程的未处理信号集设置为空集。

子进程除了继承了父进程打开的文件外,还包括:实际用户ID、实际组ID、有效用户ID、有效组ID,附加组ID、进程组ID、会话ID,控制终端、存储映射等等。

通常情况下fork都会成功,但也有可能失败。使fork失败的两个主要原因是:1、系统中已经有了太多的进程,2、该实际用户ID的进程总数超过了系统限制。

fork有下面两种用法:

1、一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。

2、一个进程要执行一个不同的程序。这对shell是常见的情况。这种情况下,子进程从fork返回后立即调用exec。

除了fork创建一个新进程外还有vfork同样用来创建一个新进程:

vfork函数的调用序列和返回值与fork相同,但两者的语义有如下几点不同:

1、vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。

2、vfork与fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会存访该地址空间。

3、vfork与fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能运行。

(二)

进程是有生命周期的,从其被创建到终止,就是其生命周期。进程的终止有8种方式,5种正常终止方式、3种异常终止方式。不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

不管是正常终止还是异常终止,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态作为参数传送给函数。在异常终止情况下,内核(而不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数去的终止状态。注意:这里使用了“退出状态”和“终止状态”两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态转换成终止状态。

关于进程的终止及退出状态,要注意一下几点:

1、子进程在父进程调用fork后生成,子进程将其终止状态返回给父进程。但如果父进程在子进程之前终止,则,对于父进程已经终止的所有进程,他们的父进程都改变为init进城。称这些进程被init进程领养。操作过程如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则将该进程的父进程ID更改为1。

2、如果子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子进程的终止状态呢?答:内核为每个终止进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态、以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。一个已经终止、但是其父进程尚未对其进行善后处理的进程称为僵死进程。

3、一个由init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?不会。因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数去的其终止状态。当提及“一个init的子进”时,这指的可能是init直接产生的进程,也可能是其父进程已终止,由init领养的进程。

(三)

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的寒素(信号处理程序)。系统默认是忽略它。现在需要知道的是调用wait或waitpid的进程可能发生什么情况:

1、如果其所有子进程都还在运行,则阻塞。

2、如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。

3、如果它没有任何子进程,则立即出错返回。

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);  // 返回值:若成功则返回进程ID,0,若出错则返回-1

两个函数的区别是:

1、在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。

2、waitpid并不等待在其调用之后的第一个终止子进程,它由若干选项,可以控制它所等待的进程。

函数的参数status是一个整型指针。如果status不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。检查wait或waitpid所返回的终止状态的宏有4个:WIFEXITED(status)、WIFSIGNALED(status)、WIFSTOPPED(status)、WIFONTINUED(stauts)。

waitpid函数提供了wait函数没有提供的三个功能:

1、waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。

2、waitpid提供了一个wait的非阻塞版本。有时用户希望取得一个子进程的状态,但不想阻塞。

3、waitpid支持作业控制。

Linux环境编程之进程(四):创建新进程、执行程序和进程终止

时间: 2024-08-05 15:00:53

Linux环境编程之进程(四):创建新进程、执行程序和进程终止的相关文章

Linux环境编程之同步(四):Posix信号量

信号量是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语.有三种类型:Posix有名信号量,使用Posix IPC名字标识:Posix基于内存的信号量,存放在共享内存区中:System V信号量,在内核中维护.这三种信号量都可用于进程间或线程间的同步. 图1 由两个进程使用的一个二值信号量 图2 由两个进程使用的一个Posix有名二值信号量 图3 由一个进程内的两个线程共享的基于内存的信号量 一个进程可以在某个信号量上执行的三种操作: 1.创建一个信号量,这要求调用者指定初始值,对

Linux环境编程之文件I/O(四):文件I/O的数据结构

(一) Linux系统支持不同进程间共享打开的文件.内核使用三种数据结构表示打开的文件:进程表项.文件表项.v节点表. 1.进程表项:每个进程在进程表中都有一个记录项,记录项中年包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项.与每个文件描述符相关联的是: a.文件描述符标志 b.指向一个文件表项的指针 2.内核为所有打开文件维持一张文件表.每个文件表项包含: a.文件状态标志,如读写.添加.同步和非阻塞等. b.当前文件偏移量 c.指向该文件v节点表项的指针 3.每个打开的文

Linux环境编程之进程(一):main函数调用、进程终止以及命令行参数和环境表

(一)main函数调用 main函数作为程序运行时的入口函数,它是如何被调用的呢?首先必须清楚一点,main函数也是一个函数,它只有被调用才能够执行.其实,在执行可执行程序时,在调用main函数之前,内核会先调用一个特殊的启动例程,将此启动例程作为可执行程序的起始地址.启动例程是如何作为可执行程序的起始地址的?这是由链接编译器设置的,而链接编译器则是由C编译器(如gcc编译器)调用的.启动例程作为可执行程序的起始地址主要做哪些工作呢?启动例程从内核取得命令行参数和环境变量值,以此来为main函数

Linux环境编程之进程(六):进程组

进程组 每个进程除了有一个进程ID之外,还属于一个进程组.进程组是一个或多个进程的集合.每个进程组有一个唯一的进程组ID.进程组ID类似于进程ID--它是一个整数,并可存放在pid_t数据类型中.函数getpgrp返回调用进程的进程组ID. 每个进程组都可以有一个组长进程.组长进程的标识是,其进程组ID等于其进程ID.组长进程可以创建一个进程组,创建该组中的进程,然后终止.只要在某个进程组中有一个进程存在,则进程组就存在,这与其组长进程是否终止无关.从进程组创建开始到其中最后一个进程离开为止的时

Linux环境编程之进程(七):守护进程

守护进程也是一种进程,它由如下特性: 1.生存期较长,在系统自举时启动,仅在系统关闭时终止. 2.没有控制终端,在后台运行. 系统中有很多守护进程,它们执行日常事务活动.如日志进程syslogd.web服务器httpd.邮件服务器sendmail和数据块服务器mysqld等.大多数守护进程都是以超级用户(用户ID为0)特权运行.没有一个守护进程具有控制终端,其终端设置为问号(?),终端前台进程组ID设置为-1.内核守护进程以无控制终端方式启动.用户层守护进程缺少控制终端可能是守护进程调用了set

Linux环境编程之进程(五):竞争条件以及exec函数

(一) 当多个进程企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,就认为它们发生了竞争关系.避免竞争的条件,给出apue上的一个代码吧: #include "apue.h" static void charatatime(char *); int main(void) { pid_t pid; TELL_WAIT(); /*set things up for TELL_XXX & WAIT_XXX*/ if((pid == fork()) < 0){ e

Linux环境编程之进程(二):程序的存储空间布局

引言: 一个写好的程序一般要存放在存储器中,那么程序中的代码.数据等各部分,是如何有规律的存放在存储器中的呢? (一) 一个存储的程序可分为五部分:正文段.初始化数据段.非初始化数据段.栈.堆.其典型的存储安排如下图: 正文段:这是由CPU执行的机器指令的部分.通常,正文段是可共享的,所以即使是频繁执行的程序在存储器中也只需要一个副本,另外正文段常常是只读的,以防止程序由于意外而修改其自身的指令. 初始化数据段:通常称为数据段,它包含了程序中需要明确地赋初值的变量. 非初始化数据段:通常称为bs

Linux环境编程之进程(三):函数间跳转

引言:在编写程序时,经常在函数内部使用goto语句来跳转,从而进行出错处理,那么如果想要在函数之间进行跳转该怎么做呢?使用setjmp和longjmp函数. 给出示例程序: #include <stdio.h> #include <stdlib.h> #include <setjmp.h> static void f1(int, int, int, int); static void f2(void); static jmp_buf jmpbuffer; static

Linux环境编程之文件I/O(六):文件属性

引言: 在Linux中使用ls -l filename命令查看filename的属性时,会列出文件的9种属性,例如:ls -l /etc/fstab -rw-r--r-- 1 root root 1102 2013-10-12 02:33 /etc/fstab 从左到右分别是类型与权限.文件个数.该文件或目录的拥有者.所属的组.文件大小.创建时间.文件名 以上这些文件属性的信息,都存放在一个stat的结构体中.下面就来分析一下这个结构体. 要想查看一个文件的stat结构体,可以通过stat类函数