进程控制

1:进程标识

进程ID:

每个进程都有一个非负整数表示的唯一进程 ID。所谓的唯一,即当前正在系统中运行的所有进程的ID各不相同,而当一个进程A终止后,它的进程 ID 可以复用。
补充:大多数UNIX系统实现的是延迟复用算法,使得新进程B的ID不同于最近终止的进程A的ID。

系统专用进程:

ID为0的进程通常是调度进程,也称作交换进程。该进程是操作系统内核的一部分,并不执行任何磁盘上的程序,因此也称作是系统进程。

ID为1的进程通常是init进程,在自举过程结束时由内核调用。该进程负责在内核自举后启动一个Unix系统,它决不会终止,是一个普通的用户进程,但以超级用户特权运行。

ID为2的进程是页守护进程,负责支持虚拟存储器系统的分页操作。

获取进程标识符的几个函数:

#include<unistd.h>

pid_t getpid(void);  // 返回值:调用进程的进程ID
pid_t getppid(void); // 返回值:调用进程的父进程ID
uid_t getuid(void);  // 返回值:返回进程的实际用户ID
uid_t geteuid(void); // 返回值:返回进程的有效用户ID
gid_t getgid(void);  // 返回值:返回进程的实际组ID
gid_t getegid(void); // 返回值:返回进程的有效组ID
//注意,这些函数都没有出错返回

2:fork函数

#include<unistd.h>

pid_t fork(void);//成功: 子进程返回 0  父进程返回子进程ID。失败:返回 -1

对于父进程和子进程返回ID不同的解释:

子进程返回值是 0 的理由:一个进程总可以通过getpid知道它的进程ID,通过getppid知道它的父进程的ID。

父进程返回值是子进程的进程ID的理由:一个进程的子进程可以有多个,但是并没有函数可以获取它的子进程的ID。

子进程是父进程的一份一模一样的拷贝,如子进程获取了父进程数据空间、堆、栈的副本。 但父子进程并不共享这些数据空间、堆、栈。父子进程共享正文段(因为正文段是只读的)。

由于在fork之后经常跟随着exec,所以现在刚开始子进程的很多实现并不执行一个父进程数据段、堆和栈的完全副本。Fork后,内核并没有为子进程分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。即copy-on-write。

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

int main()
{
    pid_t pid;
    int n = 0;
    pid = fork();
    while(1)
    {
        if(pid < 0)
        {
            perror("fork failed\n");
            exit(1);
        }
        else if(pid == 0)
        {
            n--;
            printf("child's n is:%d\n",n);
        }
        else
        {
            n++;
            printf("parent's n is:%d\n",n);
        }
        sleep(1);
    }
    exit(0);
}

运行后发现:

1:子进程和父进程之间并没有对各自的变量产生影响。
2:父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。

在APUE图8.1中,运用了sizeof操作符和strlen函数,其区别是:strlen不包括null字节,一次函数调用;sizeof包括null字节,编译时计算。

父进程中的所有打开文件描述符都被复制到子进程中,父子进程为每个相同的打开描述符共享一个文件表项,故共享同一文件偏移量。如果父子进程写同一描述符执行的文件,又没有任何形式的同步,那么它们的输出就会混合。

在fork之后处理文件描述符有以下两种常见的情况:

父进程等待子进程完成。这种情况下,父进程无需对其描述符做任何处理。

父进程和子进程各自执行不同的程序段。这种情况下,fork之后,父子进程各自关闭它们
不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。

除了打开的文件描述符之外,子进程还继承了父进程的下列属性:

实际用户ID、实际组ID、有效用户ID、有效组ID、附属组ID、进程组ID、会话ID、控制
终端、设置用户ID标志和设置组ID标志、当前工作目录、根目录、文件模式创建屏蔽字、
信号屏蔽和信号处理、对任一打开文件描述符的执行时关闭标志、环境、连接的共享存储段、存储映像、资源限制。

父进程和子进程之间的区别具体如下:

fork的返回值不同
pid不同
这两个进程的父进程不同
子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0
子进程不继承父进程设置的文件锁
子进程的未处理闹钟被清除
子进程的未处理信号集设置为空集

fork失败的两个主要原因:

系统中已经有了太多的进程
该实际用户ID的进程总数超过了系统限制

fork有以下两种用法:

一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。
这在网络服务器中是常见的。
一个进程要执行一个不同的程序。一般fork之后立即调用exec。

3:vfork函数

与fork函数相比:

相同点:返回值相同

不同点:

1.vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序,故不将父进程
的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会
引用该地址空间。在调用exec或exit之前,子进程在父进程的空间中运行。
2.vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。故如果
在调用这两个函数之前,子进程依赖于父进程的进一步动作,则会导致死锁。

为什么需要vfork?

因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要
再把数据复制上花费时间了,因此vfork就是”为了exec而生“的。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t pid;
    int n = 0;
    int i;
    pid = vfork();
    for(i = 0; i < 5; i++)
    {
        if(pid < 0)
        {
            perror("fork failed\n");
            exit(1);
        }
        else if(pid == 0)
        {
            n--;
            printf("child's n is:%d\n",n);
            if(i == 1)
            {
                _exit(0);
                //return 0;
                //exit(0);
            }
        }
        else
        {
            n++;
            printf("parent's n is:%d\n",n);
        }
        sleep(1);
    }
    exit(0);
}

这里写图片描述
运行后,可以发现子进程先被执行,exit后,父进程才被执行,同时子进程改变了父进程中的数据。
如果将子进程中的_exit改为return 0,执行结果为:
这里写图片描述

之所以会出现以上结果,是因为如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。
exit()执行结果与_exit()执行结果相同。

4:exit函数

进程有5种正常终止及3种异常终止方式。

5种正常终止方式:

从main中执行return,等效于调用exit。
调用exit函数,操作包括调用各终止处理程序,关闭标准I/O流,最后调用_exit函数。
调用_exit或_Exit,无需运行终止处理程序,Unix中不冲洗标准I/O流。
进程的最后一个线程在其启动例程执行return语句,该进程以终止状态0返回。
进程的最后一个线程调用pthread_exit,进程终止状态总是0。

3种异常终止方式:

调用abort,它产生SIGABRT信号。
当进程接收到某些信号时。这些信号可由进程自身(如调用abort函数)、其他进程或内核产生。
最后一个线程对“取消”请求做出响应。

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应的进程关闭所有打开描述符,释放它所使用的存储器等。

名词:

孤儿进程:如果父进程在子进程之前终止,则称子进程为孤儿进程。

子进程的ppid变为1,称这些进程由init进程收养。其操作过程是:在一个进程终止时,内核逐个检查所有活动进程,以判断这些活动进程是否是正要终止的进程的子进程。如果是,则该活动进程的父进程ID就改为 1这种方式确保了每个进程都有一个父进程。

僵尸进程:一个已经终止、但是等待父进程对它进行善后处理的进程称作僵尸进程。

内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait函数或者waitpid函数时,可以得到这些信息。这些信息至少包括:终止进程的进程ID、终止进程的终止状态、终止进程的使用的CPU时间总量。内核此时可以释放终止进程使用的所有内存,关闭它所有的打开文件。但是该终止进程还残留了上述信息等待父进程处理。

5:wait与waitpid函数

这两个函数用于父进程处理子进程的终止。
当一个进程终止时,内核就向其父进程发送SIGCHLD信号。这种信号是一个异步信号,因为该信号可能在任何时间发出。

父进程可以选择忽略此信号。这是系统的默认行为。
父进程也可以针对此信号注册一个信号处理程序,从而当接收到该信号时调用相应的信号处理程序。这就需要用到wait或waitpid函数。

当一个进程调用wait或waitpid函数时:

如果其所有子进程都还在运行,则阻塞。
如果一个子进程终止,正等待其父进程获取其终止状态,则取得该子进程的终止状态立即返
回。如果它没有任何子进程,则立即出错返回。
#include<sys/wait.h>

pid_t wait(int *staloc);
pid_t waitpid(pid_t pid,int *staloc,int options);
//成功:返回终止子进程的进程ID;失败:返回 0 或者 -1

参数:staloc:存放子进程终止状态的缓冲区的地址。如果你不关心子进程的终止状态,则可以设它为空指针NULL。Pid及options参数可参考APUE。

这两个函数的区别:

在一个进程终止前,wait使其调用者阻塞,而waitpid可通过选项,使调用者不阻塞。
waitpid并不等待在其调用之后的第一个终止子进程,可通过选项,控制其所等待的进程。

至于waitid、wait3、wait4函数,具有wait函数的一些加强功能,使用是可直接查看参考书。

6:exec函数

fork()函数通过系统调用创建一个与原来进程(父进程)几乎完全相同的子进程。子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。linux将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。也就是这两个进程做完全相同的事。

在fork后的子进程中使用exec函数族,可以装入和运行其它程序(子进程替换原有进程,和父进程做不同的事)。新程序从main函数开始执行。

fork创建一个新的进程就产生了一个新的PID,exec启动一个新程序,替换原有的进程,因此这个新的被 exec 执行的进程的PID不会改变(和调用exec的进程的PID一样)。

有7种不同的exec函数可以供使用,它们被统称称作exec函数:

#include<unistd.h>

int execl(const char *pathname,const char *arg0,.../*(char *) 0 */);
int execv(const char *pathname,char *const argv[]);
int execle(const char *pathname,const char *arg0,.../*(char *) 0
        ,char *const envp[] */);
int execve(const char *pathname,char *const argv[],char *const envp[]);
int execlp(const char *filename,const char*arg0,.../*(char *) 0*/);
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd,char *const argv[],char *const evnp[]);
//成功,不返回;失败,返回-1

前四个函数取路径名作为参数;后两个函数取文件名作为参数;最后一个取文件描述符做参数。

若filename中包含/,则视为路径名。
若filename不包含/,则按照PATH环境变量指定的各个目录中搜寻可执行文件。

关于exec函数的使用,可参考博客:
http://www.cnblogs.com/blankqdb/archive/2012/08/23/2652386.html
https://www.ibm.com/developerworks/cn/linux/kernel/syscall/part3/index.html

参考:
1.APUE第8章
2.http://www.cnblogs.com/DayByDay/p/3948380.html
3.http://www.cnblogs.com/blankqdb/archive/2012/08/23/2652386.html
4.http://www.cnblogs.com/jacklu/p/5317406.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值