APUE第8章 进程控制

进程标识

进程标识pid是由一个唯一的非负整数表示的。

  1. ID为0的进程一般是调度进程,通常被称为交换进程(swapper),是内核的一部分
  2. 进程ID为1的一般是init进程,负责在自举内核后,启动一个UNIX系统。init是一个普通用户进程,但是它以超级用户特权运行。
  3. ID为2是页守护进程(page daemon),负责支持虚拟存储系统的分页操作。

以下列出一些相关函数:

1
2
3
4
5
6
7
8
9
#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 getedig(void);//返回进程的有效组ID
//实际组标识用户是谁
//而有效组用于权限检查

fork函数

函数原型如下

1
2
3
#include <unistd.h>
pid_t fork(void);
//创建子进程,子进程返回0,父进程返回子进程ID
  1. 创建子进程后,子进程是父进程的副本,他们拥有同样的数据空间和堆栈(注意不是共享),共享的只有程序的正文段
  2. 书中提到了sizeof和strlen作用于字符串的时候差别,一般而言sizeof会计算空字符串,比strlen多1。而如果是字符常量的话,sizeof一般在编译的时候就计算出了结果,strlen则在调用的时候才计算,所以sizeof有时候快一些。
  3. fork的子进程会复制父进程的文件描述符,所以当父进程的标准输入输出被重定向的时候,子进程的标准输入输出都会被重定向。
  4. 子进程不会继承父进程设置的文件锁,子进程的未处理闹钟会被清除,子进程的未处理信号集会被设置为空集
  5. fork+exec组合成一个操作称为spawn

fork失败的可能原因

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

fork的应用场景

  1. 父进程希望复制自己,然后父子进程执行不同代码段,在网络服务进程中很常见。
  2. shell进程,fork后exec命令

vfork函数

返回值与fork相同,不同之处如下:

  1. vfork也会创建新的进程,不过该进程的目的是exec一个新程序,所以vfork不会让子进程完全复制父进程的地址空间,在子进程调用exec或者exit之前,会在父进程的空间中运行,这时候如果子进程修改数据,就会影响到父进程。
  2. vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。当然如果子进程调用这两个函数之前依赖于父进程的进一步操作,会导致死锁。(复习下死锁的四个必要条件:互斥条件、占有且等待、不可抢占、循环条件)

exit函数

exit和_exit、_Exit的区别在于exit函数会调用终止处理程序(atexit注册的终止处理程序)和检查打开的文件并且调用用户空间的标准I/O清理程序(如fclose),而_exit和_Exit两个函数不进行这些处理,直接返回到内核处理。

atexit函数

atexit的声明如下

1
int atexit(void (*func)(void));

传入的函数要求返回值和参数均为空,同一个函数被登记多次,也一样会执行多次,执行顺序和登记顺序相反。

正常终止情况

  1. main函数内调用return语句
  2. 调用exit函数
  3. 调用_exit或_Exit函数
  4. 进程的最后一个线程再其启动历程处返回时。此时返回值并不取决于线程返回值,而是0
  5. 进程的最后一个线程调用pthread_exit的时候,返回值同样不起作用,返回0

异常的终止情况

  1. 调用abort
  2. 进程接收到某些信号的时候
  3. 最后一个线程对“取消”请求作出响应。

最后不管如何终止,都会执行内核的一段代码,关闭所有打开的文件描述符,释放使用的存储器等。

父子进程结束时间不同带来的不同

  1. 当父进程比子进程先结束的时候,内核会检查所有进程,是否有进程是该进程的子进程,如果是的话,会将其的父进程id设为1,成为init收养的进程。
  2. 如果子进程先结束,内核会为子进程保存一定量的信息,所以父进程调用wait或waitpid的时候可以得到这些信息。
  3. 僵死进程:由于内核需要为父进程尚未处理的已结束子进程保留信息,所以这些进程叫做僵死进程。

    另外,如果是父进程是init的话,子进程结束后,init就会调用wait,故不会变成僵死进程

wait和waitpid函数

对于子进程结束时内核发出的信号,父进程默认是忽略。但是如果调用wait或者waitpid则会发生如下的可能情况

  1. 如果其所有子进程都在运行,则阻塞
  2. 如果一个子进程已经终止,则获得其终止状态,然后返回
  3. 如果该进程没有任何子进程,则出错返回

两者区别

1
2
3
#include <sys/wait.h>
pid_t wait(int *staloc);
pid_t waitpid(pid_t pid, int *staloc, int options);//POSIX版本
  1. waitpid函数中的pid参数有以下选项
    1. pid==-1的话,等待任意子进程,此时等同于wait
    2. pid>0的话,等待与pid相同的子进程,注意如果该进程没有这个pid的子进程的话,都可能出错。
    3. pid==0的话,等待组ID等于调用进程组ID的任意子进程
    4. pid<-1的话,等待组ID等于pid绝对值的任意子进程
  2. options参数(3个)可以指定对应的是否阻塞或对应作业控制等,详见APUE的P193

waitid

相比起waitpid,waitid会稍微更灵活一些

竞争条件

主要讲的是由于CPU的调度,父子间进程调度顺序和执行时间都不确定,如果有明确的顺序要求,就要使用IPC来使得进程间同步

exec

exec并不产成新的进程,而是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段
exec衍生出相应的7个执行函数,详见APUE的P199。

执行了exec命令后,基本上继承原来process上的大多数属性,但是如果文件描述符设置了FD_CLOEXEC(close-on-exec)的的话,就会关闭(当然默认关闭)。

用户ID和组ID的修改

实际用户是登录时就确定的(只有超级用户可以在进程执行的时候改变实际用户),有效用户则可在进程内执行的时候进行变化。

  1. 通过setuid(uid)修改有效用户,通过setgid(gid)来修改实际组id
  2. seteuid和setegid只修改有效用户和组
  3. 保存设置用户ID(SUID):是有效用户ID副本,既然有效用户ID是副本,那么它的作用肯定是为了以后恢复有效用户ID用的。
  4. 保存设置用户ID一般会在文件的位里面体现,即rws中的s

解释器文件

第一行是规定的格式

1
#! 解释器所在位置 参数

1
#! /bin/sh

这样子能够避免机器先用shell读取文件,然后再调用fork、exec来调用对应的解释器。直接在第一行写明的话就可以直接用解释器调用了。

system

因为system实现是调用了fork、exec、waitpid,所以有三种返回值

  1. fork失败或者waitpid返回EINTR之外的错误,则返回-1
  2. exec失败(不能执行shell),则返回值为如同shell执行了exit(127)一样
  3. 否则3个函数都成功,返回最后的shell终止状态,格式同waitpid。

getlogin

1
2
#include <unistd.h>
char* getlogin(void);

获取登录的用户名

优先级相关内容

1
2
3
4
5
6
7
#include <unistd.h>
int nice(int incr);//设置nice值,并返回nice值

#include <sys/resource.h>
int getpriority(int which, id_t who);//返回nice值
int setpriority(int which, id_t who, int value);//成功返回0,出错-1
//which和who的解释参见P221

进程时间

1
2
3
4
5
6
7
8
9
#include <sys/times.h>
clock_t times(struct tms *buf);
//其中tms结构为
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
}