APUE——Chapter 7、8:进程环境和进程控制

Chapter 7:进程环境

当内核执行C程序时,在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址。启动例程从内核取得命令行参数和环境变量值,然后为按上诉方式调用main函数做好安排。

  • 进程终止

8种方式使进程终止,

–> 5种正常终止:
(1)从main返回。
(2)调用exit。
(3)调用_exit或_Exit。
(4)最后一个线程从其启动例程返回。
(5)最后一个线程调用pthread_exit。

–> 3种异常终止:
(6)调用abort。
(7)接到一个信号并终止。
(8)最后一个线程对取消请求做出响应。

#include <stdlib.h>
void exit(int status);
void _Exit(int status);

#include <unistd.h>
void _exit(int status);

_exit和_Exit立即进入内核;
exit则先执行一些清理处理(包括调用执行各终止处理程序,关闭所有标准I/O流等),然后进入内核。

按照ISO C的规定,一个进程可以登记多达32个函数,这些函数将由exit自动调用。这些函数被称为终止处理程序(exit handler),并调用atexit函数来登记这些函数。

#include <stdlib.h>

int atexit(void (*func)(void));
                                        /* 返回值:若成功则返回0,若出错则返回非0值 */

exit调用这些函数的顺序与登记顺序相反。同一个函数若被登记多次,则会被调用多次。

这里写图片描述

内核使程序执行的唯一方法是调用一个exec函数进程自愿终止的唯一方法是显式或隐式地(通过调用exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。

  • 环境表

每个程序都会接收到一张环境表。环境表也是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。

extern char **environ;

这里写图片描述

通常用getenv和putenv函数来访问特定的环境变量,如果要查看整个环境,则必须使用environ指针。

  • C程序的存储空间布局

组成:

1)正文段(text)。CPU执行的机器指令部分。
2)初始化数据段(data)。
3)非初始化数据段(bss,block started by symbol 由符号开始的块)。
4)栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次调用函数时,其返回地址以及调用者的环境信息都存放在栈中。最近被调用的函数在栈上为其自动和临时变量分配存储空间。
5)堆。通常在堆中进行动态存储分配。

这里写图片描述

非初始化数据段的内容并不存放在磁盘上的程序文件中。其原因是:内核在程序开始运行前将它们都设置为0。
需要存在程序文件中的段只有正文段和初始化数据段

  • 存储器分配
#include <stdlib.h>

void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
                                        /* 返回值:若成功则返回非空指针,若出错则返回NULL */

void free(void *ptr);

malloc,分配指定字节数的存储区。初始值不确定。
calloc,为指定数量、指定长度的对象分配存储空间。初始化为0。
realloc,更改以前分配区的长度。

虽然sbrk可以扩充或缩小进程的存储空间,但大多数malloc和free的实现都不减小进程的存储空间。释放的空间可供以后再分配,但通常将它们保持在malloc池中而不返回给内核

注意:大多是实现所分配的存储空间比所要求的要稍微大一些额外的空间用来记录管理信息——分配块的长度、指向下一个分配块的指针等等。

致命的错误:1、释放一个已经释放的块。2、调用free时所用的指针不是三个alloc函数的返回值

  • 环境变量
#include <stdlib.h>

char *getenv(const char *name);
                                    /* 返回值:指向与name关联的value指针,若未找到则返回NULL */

int putenv(char *str);
                                    /* 返回值:若成功则返回0,若出错则返回非0值 */

int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
                                    /* 返回值:若成功则返回0,若出错则返回-1 */
  • setjmp和longjmp函数
#include <setjmp.h>

int setjmp(jmp_buf env);
                                    /* 返回值:若直接调用则返回0,若从longjmp调用返回则返回非0值 */

void longjmp(jmp_buf env, int val);

jmp_buf用来存放在调用longjmp时能用来恢复栈状态的所有信息。

  • getrlimit和setrlimit函数
#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlptr);

int setrlimit(int resource, const struct rlimit *rlptr);

进程的资源限制通常是在系统初始化由进程0建立的。

struct rlimit {
    rlim_t    rlim_cur;
    rlim_t    rlim_max;
};

这里写图片描述

普通进程只能不可逆地降低它的硬限制(且不能小于软限制)。超级用户进程或具有CAP_SYS_RESOURCE能力的进程可以随意改变它的两个限制值。

  RLIM_INFINITY值表示在一个资源上没有使用限制。在某些系统实现上,rlim_t是8字节无符号整数,而RLIM_INFINITY就是rlim_t类型的最大值。

  resource参数的常用值:

  RLIMIT_AS:进程虚拟内存(地址空间,Address Space)的最大字节长度。该限制会影响brk、mmap和mremap等。

  RLIMIT_CORE:core文件的最大字节长度。超出这个大小的core文件会被截短。指定0则表示不产生core文件。

  RLIMIT_CPU:CPU时间的使用限制(秒)。进程达到软限制时,会收到一个SIGXCPU信号(默认会终止进程。但进程可以捕获该信号)。如果进程继续消耗CPU时间,它会每秒收到一个SIGXCPU信号,直到达到硬限制,并接收到SIGKILL信号(不同的实现在此处可能会有差别)。

  RLIMIT_DATA:进程数据段(初始化数据节、未初始化数据节和堆)的最大字节长度。该限制会影响brk和sbrk等。

  RLIMIT_FSIZE:进程所能创建的文件的最大字节长度。

  RLIMIT_LOCKS:进程可创建的flock锁和fcntl租借锁的总数(租借锁是Linux特有的:fcntl可通过F_SETLEASE命令对文件加读或写的租借锁。当另一个进程尝试打开或截短该文件而产生冲突时,内核会通过信号通知持有租借锁的进程。后者应当对此作出响应,如flush缓冲区或移除租借锁等)。

  RLIMIT_MEMLOCK:进程使用mlock能够锁定在RAM中的最大字节长度(防止被换出到交换分区。内存的锁定和解锁以页为单位)。该限制会影响mlock、mlockall和mmap等。

  RLIMIT_MSGQUEUE:调用进程的实际用户所能分配的Posix消息队列的最大字节长度。

  RLIMIT_NOFILE:进程所能打开(如使用open/pipe/socket)的文件描述符的最大值加1。注意,进程间的文件描述符是独立的。超出该限制会抛出EMFILE错误。

  RLIMIT_NPROC:调用进程的实际用户所能创建进程(在Linux上,更准确的说法是线程)的最大数目。超出该限制时,fork会失败并抛出EAGAIN错误。

  RLIMIT_STACK:进程的栈的最大字节长度。超出该限制会收到SIGSEGV信号。


Chapter 8:进程控制

8.1 进程标识符

//:
每个进程都有一个非负整型表示的唯一进程ID。唯一性。进程ID可以重用。当一个进程终止后,其进程ID就可以再次使用了。

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

ID为 1 的进程通常是init进程,在自举过程结束时由内核调用。它是一个普通的用户进程

#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 */

8.2 fork函数

#include <unistd.h>

pid_t fork(void);
                                    /* 返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1 */

将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID

子进程中返回0的理由一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID

子进程是父进程的副本。子进程获得父进程数据空间、堆和栈的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中父、子进程的每个相同的打开描述符共享一个文件表项

这里写图片描述

除了打开文件之外,父进程有很多其他属性也由子进程继承,包括:
  • 实际用户ID、实际组ID、有效用户ID、有效组ID。
  • 附加组ID。
  • 进程组ID。
  • 会话ID。
  • 控制终端。
  • 设置用户ID标志和设置组ID标志。
  • 当前工作目录。
  • 根目录。
  • 文件模式创建屏蔽字。
  • 信号屏蔽和安排。
  • 针对任一打开文件描述符的在执行时关闭(close-on-exec)标志。
  • 环境。
  • 连接的共享存储段。
  • 存储映射。
  • 资源限制。

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

  • fork的返回值。
  • 进程ID不同。
  • 两个进程具有不同的父进程ID。
  • 子进程的tms_utime、tms_stime、tms_cutime、tms_ustime均被设置为0。
  • 父进程设置的文件锁不会被子进程继承。
  • 子进程的未处理的闹钟(alarm)被清除。
  • 子进程的未处理信号集设置为空集。

fork失败的两个主要原因是:

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

fork有两种用法:

  • 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。例如:网络服务进程。
  • 一个进程要执行一个不同的程序。例如:shell,子进程从fork返回后立即执行exec。

8.3 vfork函数

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

vfork和fork一样创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec,于是也就不会存访问该地址空间。相反,在子进程调用exec或exit之前,它在父进程的空间中运行

vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。(若子进程依赖父进程的进一步动作,可能死锁)

8.4 exit函数

进程的5种正常终止方式:
  • 在main函数内执行return语句。等效于exit。
  • 调用exit函数。
  • 调用_exit或_Exit函数。
  • 进程的最后一个线程在其启动例程中执行返回语句。然后该进程以终止状态0返回。
  • 进程的最后一个线程调用pthread_exit函数。
进程的3种异常终止方式:
  • 调用abort。
  • 当进程接收到某些信号时。
  • 最后一个线程对“取消”(cancellation)请求做出响应。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

一个已经终止、但其父进程尚未对其进程善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵死进程(zombie)。ps(1)命令中状态为Z。

8.5 wait和waitpid函数

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。

#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
                            /* 两个函数返回值:若成功则返回进程ID,0,若出错则返回-1 */

区别:

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

statloc获取终止状态status。

这里写图片描述

waitpid

对于waitpid函数中的pid参数

pid == -1    等待任一子进程。
pid > 0      等待其进程ID与pid相等的子进程。
pid == 0     等待其组ID等于调用进程组ID的任一子进程。
pid < -1     等待其组ID等于pid绝对值的任一子进程。

options可以是0,或者以下按位“或”
这里写图片描述

waitpid提供了wait函数没有提供的三个功能:
(1)waitpid可等待一个特定的进程。
(2)waitpid提供了一个wait的非阻塞版本。
(3)waitpid支持作业控制。

8.6 waitid函数

取进程终止状态。

#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
                            /* 若成功则返回0,若出错则返回-1 */

waitid允许一个进程指定要等待的子进程。

这里写图片描述

8.7 wait3和wait4函数

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
                        /* 若成功则返回进程ID,若出错则返回-1 */

参数rusage
该参数要求内核返回由终止进程及其所有子进程使用的资源汇总。

资源统计信息包括:用户CPU时间总量、系统CPU时间总量、页面出错次数、接收到信号的次数等。

这里写图片描述

8.8 exec函数

当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变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[]);

                        /* 6个函数返回值:若出错则返回-1,若成功则不返回值 */

8.9 更改用户ID和组ID

#include <unistd.h>

int setuid(uid_t uid);
int setgid(gid_t gid);

8.10 解释器文件

8.11 system函数

#include <stdlib.h>

int system(const char *cmdstring);

system在实现中,调用了fork、exec和waitpid。

8.12 进程会计

一个选项用于进程会计(process accounting)处理。启用该选项后,每当进程结束时,内核就会写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令、所使用的CPU时间总量、用户ID和组ID、启动时间。

函数acct启用和禁用进程会计。 accton(8)命令使用这一函数。

会计记录结构定义在头文件

#include <sys/acct.h>

typedef u_short comp_t;

struct acct {
    char ac_flag;    /* 记账标记 */
    char ac_stat;    /* termination status */

    uid_t ac_uid; /* 记账用户 ID */
    gid_t ac_gid; /* 记账组 ID */
    dev_t ac_tty; /* 控制终端 */
    time_t ac_btime; /* 进程创建时间(从开机起的秒数) */
    comp_t ac_utime; /* 用户 CPU 时间 */
    comp_t ac_stime; /* 系统 CPU 时间 */
    comp_t ac_etime; /* 流失的时间 */
    comp_t ac_mem; /* 平均内存用量 (kB) */
    comp_t ac_io; /* Characters transferred (未使用) */
    comp_t ac_rw; /* 读写的块 (未使用) */
    char ac_comm[8];   /* 命令名 (执行文件名;以0结尾) */
};

8.13 用户标识

获取登录名。

#include <unistd.h>

char *getlogin(void);

8.10 进程时间

#include <sys/times.h>

clock_t times(struct tms *buf);

                /* 返回值:若成功则返回流逝的墙上时钟时间(单位:时钟滴答数),若出错则返回-1 */
struct tms {
    clock_t    tms_utime;    /* user CPU time */
    clock_t    tms_stime;    /* system CPU time */
    clock_t    tms_cutime;   /* user CPU time, terminated children */
    clock_t    tms_cstime;   /* system CPU time, terminated children */
}

我们可以测量三种时间:墙上时钟时间、用户CPU时间、系统CPU时间。

times函数返回墙上时钟时间,此值是相对于过去的某一时刻测量的。调用两次,计算差值,就是墙上时钟时间。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值