(ˇˍˇ) 想~
本文来自《UNIX环境高级编程》的读书笔记。
- 进程的创建
- 执行程序
- 进程的终止
- 进程的属性
- 进程间的关系
- 进程的控制关系
0.进程的基本
进程的标识:每个进程都有一个非负整数表示的唯一进程ID。进程ID是唯一的,是可以作为识别进程的唯一标识的。
特殊进程ID
ID为0的进程:调度进程,也被称为交换进程(swapper)。该进程是内核的一部分,系统进程。
ID为1的进程:init进程,在自举结束之后由内核调用。/sbin/init 这个进程负责启动整个系统,读取一些系统的初始化文件,并将系统引导到一个状态。init进程绝不会终止。他不是系统进程,属于用户进程,超级用户特权运行。
ID为2的进程:有些unix在虚拟存储实现中,这个进程为页守护进程。
除了进程ID,进程还有其他的标识,如下:
#include <unistd.h>
pid_t getpid(void); //返回调用进程的ID
pid_t getppid(void); //调用进程的父进程ID
pid_t getuid(void); //调用进程的实际用户id
pid_t geteuid(void); //调用进程的有效用户id
pid_t getgid(void); //调用进程的实际用户组id
pid_t getegid(void); //调用进程的有效组ID
关于uid,euid的差别和作用可以参考下面这篇文章的一个问题的讨论:
探讨php调用外部程序的一些细节
1.fork进程的创建
子进程函数:
#include <unistd.h>
pid_t fork(void);
//返回值:子进程返回0,父进程返回子进程的ID;出错返回-1
fork函数调用一次,返回两次,子进程返回0,父进程返回子进程ID。
这样做的原因:(1)使子进程返回0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid来获取其父进程的id。(2)使父进程返回子进程的id的原因:首先一个父进程可以有多个子进程,然后没有一个函数可以获取一个进程所有子进程的id号。
调用之后的结果:子进程实际上就是父进程的一个副本(子进程获得父进程的数据空间,堆和栈的副本)。子进程和父进程不共享这些存储空间,他们只是共享正文段。
改进
上面可以很清楚的看出,子进程复制了一份父进程的副本,这样会感觉没必要,浪费。这时候需要一门技术:写时复制(copy-on-write)
写时复制
:数据只有一份,先不全部都复制给你,你要读可以,随便读。但是当你要写的时候,我就复制一份副本给你。参考:写时复制
几个点
1.fork之后子进程和父进程谁先执行的问题,不确定,这个是看操作系统的调度算法的。
2.fork之后,子进程拥有父进程的数据空间,堆栈。
3.父进程和子进程对文件的共享,文件是只有一份的,但是复制了一份文件描述符的表。如果不同步的话,就会造成子进程和父进程写文件混乱。
对文件描述符的操作的情况:
(1)父进程等待子进程完成。父进程不用对文件描述符做任何处理。当子进程终止之后,文件偏移量会更新。
(2)父进程和子进程执行的程序段不同时。他们的文件描述符互相不影响,父进程和子进程会自关闭他们不需要的文件描述符,这样也不会干扰别人使用文件描述符。
fork用法
1.fork+exec 创建子进程之后执行其他程序。
2.fork之后,子进程去运行其他代码段。
2.vfork
vfork与fork的差别:
(1)vfork用于创建一个新的进程,且新进程的目的就是exec一个新的程序。
(2)vfork与fork一样都创建一个子进程,但是它并不会将父进程的地址空间复制到子进程中,因为vfork的子进程会立即调用exec(或exit),所以不需要存放该地址空间。(vfork的子进程在父进程的空间中运行)
(3)vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。
对于父进程已经结束的进程,它们的父进程都会改变为init进程。init进程收养。
“僵尸进程”:一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占有的资源)的进程称为僵尸进程。
3.exit
exit是标准函数库定义的函数。
_exit()和_Exit()
1.main函数的return语句相当于exit
2.二者的差别在于exit会去刷新输出缓冲,而_exit取决于实现。
异常终止情况:
1.调用abort,产生SIGABRT信号。
2.当进程接受某个信号的时候,信号可由进程本身产生,或者其他的进程和内核产生。
3.最后一个线程对“取消”请求作出响应。
4.wait 和 waitpid
当一个进程正常或者异常终止的时候,内核就向父进程发送SIGCHLD,子进程结束是个异步事件,所以这对父进程是个异步事件。系统默认是忽略这个信号的,但也是可以根据这个信号做一些操作的。
调用wait或者waitpid可能会发生什么:
- 如果所有子进程都在运行,则阻塞。
- 如果子进程已经终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
如果接受到SIGCHLD信号而调用wait的时候,可能会出现阻塞。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *staloc, int options);
//返回值:成功,返回进程ID,失败,返回0或者-1
两个函数区别:
- 在一个子进程终止之前,wait使其调用者阻塞,而waitpid有一个选项可以使调用者不阻塞。
- waitpid并不等待在其调用之后的第一个终止的子进程,它有若干个选项,可以控制它所等待的进程。
- waitpid等待某个特定的进程。
waitpid的pid参数作用解释:
pid==-1 等待任一子进程。此种情况下,waitpid与wait等效。
pid>0 等待进程ID与pid相等的子进程。
pid==0 等待组ID等于调用进程组ID的任意子进程
pid<-1 等待组id等于pid绝对值的任一子进程。。
5.waitid
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options)
//返回值:若成功,返回0;若出错,返回-1
这个函数使用特定的方式来决定等待那个子进程。
idtype和id的关系
常量 | 说明 |
---|---|
P_PID | 等待一特定的进程,id包含要等待子进程的ID |
P_PGID | 等待一特定进程组的任一子进程;id包含要等待子进程的进程组ID |
P_ALL | 等待任一进程,忽略id |
option参数按照各标志位或运算。
常量 | 说明 |
---|---|
WCONTINUED | 等待一进程,它以前曾被停止,此后又继续,但其状态尚未报告。 |
WEXITED | 等待已经退出的进程 |
WONHANG | 如无可用的子进程退出状态,立即返回而非阻塞 |
WNOWAIT | 不破坏子进程退出状态,该子进程退出之后可以由后续的wait,waitid或者waitpid调用取得 |
WSTOPPED | 等待一进程,它已经停止,但其状态尚未报告。 |
6.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);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
//若成功,返回进程ID,失败返回-1
查看进程系统资源统计信息。
7.竞争条件
定义:当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件。
在apue.h中有一套TELL和WAIT封装的函数来解决竞争条件。
同步
8.函数exec
exec调用一个新的程序,从其main开始运行,exec只是用磁盘上的一个新程序替换当前进程的正文段,数据段,堆段和栈段。
unix有7中exec函数可以使用。
unix的控制原语:fork创建新进程,exec执行新的程序,exit函数处理终止,和wait函数等待终止。
#include <unistd.h>
int execl(const char *pathname, const char* arg0, ... /*(cahr*)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /*(cahr*)0,char *const envp[]*/)
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ...);
int execvp(const char *filename, char *const argv[])
int fexecve(int fd, char *const argv[], char *const envp[]);
区别:
- 前面四个函数取路径名作为参数,后两个函数取文件名作为参数,最后一个使用文件描述符作为参数。
- 第二个区别是:参数表的传递(l表示list,v表示vector)。
- 最后一个区别是以e结尾的函数可以传递一个指向环境字符串指针数组的指针
注意,在exec前后实际用户ID和实际组ID(real ID)保持不变,而有效ID(effect ID)是否改变取决于所执行的程序文件设置用户ID位和设置组ID位(setuid)。如果新程序的设置用户ID位已经设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。
七个函数之间的关系如下图:
有一个程序可以练习一下:
9.更改用户ID和组ID
在unix系统中,特权的管理以及访问控制主要依靠的是用户ID和组ID来控制的。
在设计应用中我们一般使用的是最小特权模型。只给我们程序完成给定任务所需要的最小特权。
setuid与setgid可以更改实际ID和有效ID
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
//若成功,返回0;若失败,返回-1
规则:
- 若是进程具有超级用户特权,则setuid函数将实际用户ID,有效用户ID以及保存的设置用户ID设置为uid
- 若进程没有超级用户特权,但是uid等于实际用户ID或者保存的设置用户ID,则setuid只将有效用户ID设置为uid,不更改实际用户ID和保存的设置用户ID。(这点很重要)
- 如果上面的条件都不满足,则errno设置为EPERM,并返回-1
注意:
- 只有超级用户可以改变实际用户ID。实际用户ID是在用户登录时,由login(1)程序设置的,并且绝不会改变它。因为login是一个超级用户进程,当它调用setuid的时候改变三个用户ID
- 仅当程序设置了设置用户ID的时候,exec函数才设置有效用户ID。
- 保存的设置用户ID是由exec复制有效用户ID而得到的,如果设置了文件的设置用户ID,则在exec根据文件的用户ID设置了进程的有效用户ID以后,这个副本才被保存起来。
seteuid与setegid
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
10.解释器文件
脚本前面的第一行的意思:
#! pathname [optional-argument]
经常使用的如下:
#!/bin/sh
解释器文件的第一行有长度限制的。
11.函数system
ISO C定义了system函数,POSIX包含了system接口
#include <stdlib.h>
int system(const char *cmdstring);
一些返回值:
- 当cmdstring为空指针的时候,则仅当命令处理程序可用时,system返回非0值。
- system在其实现中调用了fork,exec和waitpid,因此,当fork失败的时候,返回-1
- 如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit
- 否则三个函数(fork,exec,eaitpid)都成功,那么system返回值是shell的终止状态。
system的实现:
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
//没有信号处理的版本
int system(const char *cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL)
{
return -1;
}
if((pid = fork()) < 0)
{
status = -1;
}
else if(pid == 0)
{
execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
_exit(127);
}
else
{
while(waitpid(pid, &status, 0) < 0)
{
if(errno != EINTR)
{
status = -1;
break;
}
}
}
return status;
}
12.进程的调度
进程的调度策略和调度优先级是由内核确定的。进程可以通过调整nice值来选择以更低的优先级运行(通过调整nice值降低它对CPU的占有,因此该进程是友好的)。只有特权进程允许提高调度权限。
主要讲述使用nice函数来改变进程的优先级的