uc笔记07---进程管理,PID,#ps,getxxxid,fork,vfork,system
2015-09-30 17:00
423 查看
1. 进程与程序
1) 进程就是运行中的程序。一个运行着的程序,
可能有多个进程。进程在操作系统中执行特定的任务。
2) 程序是存储在磁盘上,包含可执行机器指令和数据的静态实体。
进程或者任务是处于活动状态的计算机程序。
2. 进程的分类
1) 进程一般分为 交互进程、批处理进程 和 守护进程三类。
2) 守护进程总是活跃的,一般是后台运行。
守护进程一般是由系统在开机时通过脚本自动激活启动,或者由超级用户 root 来启动。
3. 查看进程
1) 简单形式
# ps
以简略方式显示当前用户有控制终端的进程信息。
2) BSD 风格常用选项
# ps axu
a - 所有用户有控制终端的进程
x - 包括无控制终端的进程
u - 以详尽方式显示
w - 以更大列宽显示
3) SVR4 风格常用选项
# ps -efl
# ps -efl | grep 10086 // 查看具体某一个进程
# ps -efl | grep a.out
这里 | 是管道符,意思是把前者的输出当作后者的输入;
-e或-A - 所有用户的进程
-a - 当前终端的进程
-u 用户名或用户ID - 特定用户的进程 // #ps -u root
-g 组名或组ID - 特定组的进程 // #ps -g tarena
-f - 按完整格式显示
-F - 按更完整格式显示
-l - 按长格式显示
4) 进程信息列表
USER/UID: 进程属主。
PID: 进程 ID。
%CPU/C: CPU 使用率。
%MEM: 内存使用率。
VSZ: 占用虚拟内存大小(KB)。
RSS: 占用物理内存大小(KB)。
TTY: 终端次设备号,“?”表示无控制终端,如后台进程。
pts/1:虚拟终端,tty3:实体终端;
STAT/S: 进程状态。可取如下值:
O - 就绪。等待被调度。
R - 运行。Linux 下没有 O 状态,就绪状态也用 R 表示。
S - 可唤醒睡眠。系统中断,获得资源,收到信号,都可被唤醒,转入运行状态。
D - 不可唤醒睡眠。只能被 wake_up 系统调用唤醒。
T - 暂停。收到 SIGSTOP 信号转入暂停状态,收到 SIGCONT 信号转入运行状态。
W - 等待内存分页 (2.6 内核以后被废弃)。
X - 死亡。不可见。
Z - 僵尸。已停止运行,但其父进程尚未获取其状态。
< - 高优先级。
N - 低优先级。
L - 有被锁到内存中的分页。实时进程和定制 IO。
s - 会话首进程。
l - 多线程化的进程。
+ - 在前台进程组中。
START/STIME: 进程开始时间。
TIME: 进程运行时间。
COMMAND/CMD: 进程指令。
F: 进程标志。可由下列值取和:
1 - 通过 fork 产生但是没有 exec。
4 - 拥有超级用户特权。
PPID: 父进程 ID。
#ps -f
NI: 进程 nice 值,-20 到 19,可通过系统调用或命令修改。
PRI: 进程优先级。
#ps -lF
静态优先级 = 80 + nice,60 到 99,值越小优先级越高。
内核在静态优先级的基础上,根据进程的交互性计算得到实际(动态)优先级,
以体现对 IO 消耗型进程的奖励,和对处理器消耗型进程的惩罚。
ADDR: 内核进程的内存地址。普通进程显示“-”。
SZ: 占用虚拟内存页数。
WCHAN: 进程正在等待的内核函数或事件。
PSR: 进程被绑定到哪个处理器。
例如:
# ps axu
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2172 668 ? Ss 09:03 0:00 init [5]
root 2 0.0 0.0 0 0 ? S< 09:03 0:00 [migration/0]
root 3 0.0 0.0 0 0 ? SN 09:03 0:00 [ksoftirqd/0]
root 4 0.0 0.0 0 0 tty4 S< 09:03 0:00 [watchdog/0]
root 5 0.0 0.0 0 0 pts/1 S< 09:03 0:00 [events/0]
. . .
# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 500 3223 3217 0 75 0 - 1512 wait pts/1 00:00:00 bash
0 R 500 4561 3223 0 77 0 - 1356 - pts/1 00:00:00 ps
4. 父进程、子进程、孤儿进程和僵尸进程
内核进程(0)
init(1)
xinetd
in.telnetd <- 用户登录
login
bash
vi
1) 父进程启动子进程后,
子进程在操作系统的调度下与其父进程同时运行。
2) 子进程先于父进程结束,
子进程向父进程发送 SIGCHLD(17) 信号,父进程回收子进程的相关资源。
如果父进程没有回收子进程的相关资源,该子进程即成为僵尸进程。
3) 父进程先于子进程结束,子进程成为孤儿进程,
同时被 init 进程收养,即成为 init 进程的子进程。
tips:孤儿进程和僵尸进程不可能同时存在一个进程上;
5. 进程标识符(进程 ID)
1) 每个进程都有一个以非负整数表示的唯一标识,即进程 ID/PID。
2) 进程 ID 在任何时刻都是唯一的,但可以重用,当一个进程退出时,其进程 ID 就可以被其它进程使用。
3) 延迟重用。
范例:delay.c
#include <stdio.h>
int main (void) {
printf ("进程ID:%u\n", getpid ());
return 0;
}
运行测试:
#a.out
进程ID:16431
#a.out
进程ID:16432
#a.out
进程ID:16433
#a.out
进程ID:16431
6. getxxxid
#include <unistd.h>
getpid - 获取进程ID
getppid - 获取父进程ID
getuid - 获取实际用户ID(登录用户 ID)
geteuid - 获取有效用户ID(属主 ID)
getgid - 获取实际组ID
getegid - 获取有效组ID
范例:id.c
#include <stdio.h>
int main (void) {
printf (" 进程ID:%u\n", getpid ());
printf (" 父进程ID:%u\n", getppid ());
printf ("实际用户ID:%u\n", getuid ());
printf ("有效用户ID:%u\n", geteuid ());
printf (" 实际组ID:%u\n", getgid ());
printf (" 有效组ID:%u\n", getegid ());
return 0;
}
以其它用户身份登录并执行
# a.out
输出
进程ID:16443
父进程ID:15890
实际用户ID:1000 - 实际用户 ID 取父进程(shell)的实际用户 ID
有效用户ID:1000 - 有效用户 ID 取实际用户 ID
实际组ID:1000 - 实际组 ID 取父进程(shell)的实际组 ID
有效组ID:1000 - 有效组 ID 取实际组 ID
查看进程 ID:
#ps -eaf | grep 15890
执行
# ls -l a.out
输出
-rwxr-xr-x ...
^ ^
为a.out的文件权限添加设置用户ID位和设置组ID位
# chmod u+s a.out
# chmod g+s a.out
执行
# ls -l a.out
输出
-rwsr-sr-x ...
^ ^
以其它用户身份登录并执行
# a.out
输出
进程ID:...
父进程ID:...
实际用户ID:1000 - 实际用户 ID 取父进程(shell)的实际用户 ID
有效用户ID:0 - 有效用户 ID 取程序文件的属主 ID
实际组ID:1000 - 实际组 ID 取父进程(shell)的实际组 ID
有效组ID:0 - 有效组 ID 取程序文件的属组 ID
进程的访问权限由其有效用户 ID 和有效组 ID 决定。
通过此方法可以使进程获得比登录用户更高的权限。
比如通过 passwd 命令修改登录口令:
执行
ls -l /etc/passwd
输出
-rw-r--r--. 1 root root 1648 Nov 9 14:05 /etc/passwd
^
该文件中存放所有用户的口令信息,仅 root 用户可写,
但事实上任何用户都可以修改自己的登录口令,
即任何用户都可以通过 /usr/bin/passwd 程序写该文件:
执行
# ls -l /usr/bin/passwd
输出
-rwsr-xr-x. 1 root root 28816 Feb 8 2011 /usr/bin/passwd
^ ^
该程序具有设置用户 ID 位,且其属主为 root。
因此以任何用户登录系统,执行 passwd 命令所启动的进程,
其有效用户 ID 均为 root,对 /etc/passwd 文件有写权限。
7. fork
#include <unistd.h>
pid_t fork (void);
1)创建一个子进程,失败返回 -1。
2)调用一次,返回两次;在父进程中返回子进程的 PID;在子进程中返回 0;
利用返回值的不同,可以分别为父/子进程编写不同的处理分支。
范例:fork.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("%u进程:我要调用fork()了...\n", getpid ());
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) { // fork() 在子进程里返回 0;
printf ("%u进程:我是%u进程的子进程。\n", getpid (),
getppid ());
return 0;
}
printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
sleep (1);
return 0;
}
3)子进程是父进程的副本,
子进程获得父进程数据段和堆栈段(包括 I/O 流缓冲区)的拷贝,
但子进程共享父进程的代码段。
范例:mem.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int global = 100;
int main (void) {
int local = 200;
char* heap = (char*)malloc (256 * sizeof (char));
sprintf (heap, "ABC");
printf ("父进程:%d %d %s\n", global, local, heap);
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
// 在子进程里对三个数据作修改
if (pid == 0) {
global++;
local++;
sprintf (heap, "XYZ");
printf ("子进程:%d %d %s\n", global, local, heap);
free (heap);
return 0;
}
sleep (1);
printf ("父进程:%d %d %s\n", global, local, heap);
free (heap);
return 0;
}
输出结果:
父进程:100 200 ABC
子进程:101 201 XYZ
父进程:100 200 ABC
范例:os.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("ABC"); // 放到缓冲区
// 开始分支
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
printf ("XYZ\n");
return 0;
}
sleep (1);
printf ("\n");
return 0;
}
输出:
ABCXYZ // 子进程输出,把父进程的缓冲区复制过来;
ABC // 父进程输出;
范例:is.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("父进程:");
int a, b, c;
scanf ("%d%d%d", &a, &b, &c);
// 开始分支
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
scanf ("%d%d%d", &a, &b, &c);
printf ("子进程:%d %d %d\n", a, b, c);
return 0;
}
sleep (1);
printf ("父进程:%d %d %d\n", a, b, c);
return 0;
}
输入:
父进程:123456
输出:
子进程:456
父进程:123
4)函数调用后父子进程各自继续运行,
其先后顺序不确定,某些实现可以保证子进程先被调度。
5)函数调用后,父进程的文件描述符表(进程级)也会被复制到子进程中,
二者共享同一个文件表(内核级)。
范例:ftab.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void) {
int fd = open ("ftab.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror ("open");
return -1;
}
// 父进程写入一个字符串
const char* text = "Hello, World !"; // 文件指针在 ! 之后那个位置
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
// 进入子进程
pid_t pid = fork ();
if (pid == -1) { // 进入子进程失败
perror ("fork");
return -1;
}
if (pid == 0) { // 进入子进程成功
if (lseek (fd, -7, SEEK_CUR) == -1) { // 从当前位置向文件首移动 7 字节,应该在 W 位置
perror ("lseek");
return -1;
}
close (fd); // 只能关闭子进程的文件描述符,而无法关闭文件表,应为父进程还在用;
return 0;
}
sleep (1);
// 应为文件表共享,所以在子进程里面移动了文件位置,对父进程也有影响
text = "Linux";
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
// 最后 ftab.txt 里面将显示:Hello Linux
close (fd);
return 0;
}
6)总进程数或实际用户 ID 所拥有的进程数,超过系统限制,该函数将失败。
7)一个进程如果希望创建自己的副本并执行同一份代码,
或希望与另一个程序并发地运行,都可以使用该函数。
8)孤儿进程与僵尸进程。
范例:orphan.c(孤儿进程)
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("%u进程:我要调用fork()了...\n", getpid ());
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
sleep (1); // 睡一会,等待父进程死去;
// 验证:如果是孤儿,则 PPID 为 1(init);如果不是,则为上面的 %u 的值;
printf ("\n%u进程:我是被%u进程收养的孤儿进程。", getpid (),
getppid ());
return 0;
}
// 父进程打印完这条语句就死去;
printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
return 0;
}
范例:zombie.c(僵尸进程)
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("%u进程:我要调用fork()了...\n", getpid ());
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
// 打完下面这段话,子进程就死去;
printf ("%u进程:我是%u进程的子进程,即将成为僵尸...\n",
getpid (), getppid ());
return 0;
}
// 父进程睡一会,等子进程死去;
sleep (1);
printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
// 此时查看子进程状态
printf ("执行ps -efl | grep %u,按<回车>退出...", pid);
getchar (); // 创造阻塞条件,只有当父进程结束,init 才回收子进程;
return 0;
}
注意:fork 前的代码只有父进程执行,
fork 后的代码父子进程都有机会执行,受代码逻辑的控制而进入不同分支。
8. vfork
#include <unistd.h>
pid_t vfork (void);
该函数的功能与 fork 基本相同,二者的区别:
1)调用 vfork 创建子进程时并不复制父进程的地址空间,
子进程可以通过 exec 函数族,直接启动另一个进程替换自身,进而提高进程创建的效率。
2)vfork 调用之后,子进程先被调度。
用 fork 调用的话,可能是子进程先被调用,也可能是父进程先被调用;
9. 进程的正常退出
从 main 函数中 return(表示整个程序退出)
int main (...) {
. . .
return x;
}
等价于:
int main (...) {
. . .
exit (x);
}
调用标准 C语言的 exit 函数;
在任何函数里调用 exit(); 都会直接退出整个进程;
#include <stdlib.h>
void exit (int status);
1) 调用进程退出,
其父进程调用 wait/waitpid 函数返回 status 的低 8 位。
2) 进程退出之前,
先调用所有事先通过 atexit/on_exit 回调函数注册的函数,
冲刷并关闭所有仍处于打开状态的标准 I/O 流,删除所有通过 tmpfile 函数创建的文件。
#include <stdlib.h>
int atexit (void (*function) (void));
function - 函数指针,指向进程退出前需要被调用的函数。
该函数既没有返回值也没有参数。
成功返回 0,失败返回非零。
int on_exit (void (*function) (int, void*), void* arg);
function - 函数指针,指向进程退出前需要被调用的函数。
该函数没有返回值但有两个参数:
第一参数来自 exit 函数的 status 参数,
第二个参数来自 on_exit 函数的 arg 参数。
arg - 任意指针,将作为第二个参数被传递给 function 所指向的函数。
成功返回 0,失败返回非零。
3) 用 EXIT_SUCCESS/EXIT_FAILURE 常量宏
(可能是 0/1)作参数,调用 exit() 函数表示成功/失败,提高平台兼容性。
4) 该函数不会返回。
因为调完该函数,进程就结束了,无法返回;
5) 该函数的实现调用了 _exit/_Exit 函数。
10. 调用 _exit/_Exit 函数。
#include <unistd.h>
void _exit (int status);
1) 调用进程退出,
其父进程调用 wait/waitpid 函数返回 status 的低 8 位。
2) 进程退出之前,
先关闭所有仍处于打开状态的文件描述符,
将其所有子进程托付给 init 进程(PID 为 1 的进程)收养,向父进程递送 SIGCHILD 信号。
3) 该函数不会返回。
4) 该函数有一个完全等价的标准 C 版本:
#include <stdlib.h>
void _Exit (int status);
进程的最后一个线程执行了返回语句。
进程的最后一个线程调用 pthread_exit 函数。
5)小结:_exit 是系统调用,_Exit 是标 C 函数;
exit 在底层调用 _exit,在调用 _exit 之前会先调用回调函数 atexit/on_exit;
范例:exit.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void doexit1 (void) {
printf ("doexit1()...\n");
}
void doexit2 (int status, void* arg) {
printf ("doexit2(%d,%s)...\n", status, arg);
}
int foo (void) {
printf ("foo()...\n");
// exit (EXIT_SUCCESS);
// exit (EXIT_FAILURE);
exit (1234);
// _exit (1234); 不调用 doexit1 和 doexit2
// _Exit (1234); 不调用 doexit1 和 doexit2
return 10;
}
int main (void) {
if (atexit (doexit1) == -1) {
perror ("atexit");
return -1;
}
if (on_exit (doexit2, "再见") == -1) {
perror ("on_exit");
return -1;
}
printf ("foo()函数返回%d。\n", foo ());
// return 1234; main 函数里面的 return 相当于调用 exit();
}
输出结果如下:
foo()...
doexit2(1234, 再见)...
doexit1()...
分析:主函数执行到 printf ("foo()函数返回%d。\n", foo ()); 时,
调用 foo() 函数,foo() 调用 exit;exit 调用 atexit 和 on_exit;
当 atexit/on_exit 执行完后,程序就结束了;
不会执行 printf ("foo()函数返回%d。\n", foo ()); 里的 "foo()函数返回%d。\n" 语句;
11. 进程的异常终止
1)调用 abort 函数,产生 SIGABRT 信号。
2)进程接收到某些信号。
3)最后一个线程对“取消”请求做出响应。
12. 等待子进程终止并获取其终止状态 wait/waitpid(收尸)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int* status); // 等待所有子进程
pid_t waitpid (pid_t pid, int* status, int options); // 等待具体某一个子进程
成功返回终止子进程的 PID,失败返回 -1。
1)当一个进程正常或异常终止时,内核向其父进程发送 SIGCHLD 信号。
父进程可以忽略该信号,或者提供一个针对该信号的信号处理函数,默认为忽略。
2)父进程调用 wait 函数:
若所有子进程都在运行,则阻塞。
若有一个子进程已终止,则返回该子进程的 PID 和终止状态 (通过 status 参数)。
若没有需要等待子进程,则返回失败 -1,errno 为 ECHILD。
3)在任何一个子进程终止前,wait 函数只能阻塞调用进程,而 waitpid 函数可以有更多选择。
4)如果有一个子进程在 wait 函数被调用之前,
已经终止并处于僵尸状态,wait 函数会立即返回,并取得该子进程的终止状态。
5)子进程的终止状态通过输出参数 status 返回给调用者,若不关心终止状态,可将此参数置空。
6)子进程的终止状态可借助 sys/wait.h 中定义的参数宏查看:
WIFEXITED(): 子进程是否正常终止,是则通过 WEXITSTATUS() 宏,
获取子进程调用 exit/_exit/_Exit 函数,所传递参数的低 8 位。
因此传给 exit/_exit/_Exit 函数的参数最好不要超过 255。
WIFSIGNALED(): 子进程是否异常终止,是则通过 WTERMSIG() 宏获取终止子进程的信号。
WIFSTOPPED(): 子进程是否处于暂停,是则通过 WSTOPSIG() 宏获取暂停子进程的信号。
WIFCONTINUED(): 子进程是否在暂停之后继续运行。
范例:wait.c
#include <stdio.h>
#include <sys/wait.h>
int main (void) {
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
int status = 0x12345678;
printf ("子进程:我是%u进程。我要以%#x状态退出。\n", getpid (), status);
// 输出:子进程:我是 20163 进程。我要以 0x12345678 状态退出。
return status;
}
printf ("父进程:我要等待子进程...\n");
int status;
pid = wait (&status); // wait 会一直等到子进程退出;
printf ("父进程:发现%u进程以%#x状态退出了。\n", pid, WEXITSTATUS (status));
// wait 通过 WEXITSTATUS 得到子进程终止状态;
// WEXITSTATUS 取得所传参数 0x12345678 的低 8 位:0x78
// 输出:父进程:发现 20163 进程以 0x78 状态退出了。
return 0;
}
范例:loop.c
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int main (void) {
int i;
for (i = 0; i < 3; i++) {
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
return 0;
}
}
for (;;) {
printf ("父进程:我要等待子进程...\n");
pid_t pid = wait (0); // 调一次 wait 只能回收一个子进程,所以需要不停循环;
if (pid == -1) { // 返回 -1 有两种情况:子进程都在运行,或者子进程都回收完了;
if (errno != ECHILD) { // 异常退出;
perror ("wait");
return -1;
}
printf ("父进程:已经没有子进程可等了,走喽!\n");
break;
}
printf ("父进程:发现%u进程退出了。\n", pid);
}
return 0;
}
7)如果同时存在多个子进程,又需要等待特定的子进程,可使用 waitpid 函数,其 pid 参数:
pid_t waitpid (pid_t pid, int* status, int options);
-1 - 等待任一子进程,此时与 wait 函数等价。
> 0 - 等待由该参数所标识的特定子进程。
0 - 等待其组 ID 等于调用进程组 ID 的任一子进程,
即等待与调用进程同进程组的任一子进程。
<-1 - 等待其组 ID 等于该参数绝对值的任一子进程,
即等待隶属于特定进程组内的任一子进程。
范例:waitpid.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
pid_t cpid[3];
int i;
for (i = 0; i < sizeof (cpid) / sizeof (cpid[0]); i++) {
cpid[i] = fork ();
if (cpid[i] == -1) {
perror ("fork");
return -1;
}
if (cpid[i] == 0) {
printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
return 0;
}
}
for (i = 0; i < sizeof (cpid) / sizeof (cpid[0]); i++) {
printf ("父进程:我要等待%u进程...\n", cpid[i]);
pid_t pid = waitpid (cpid[i], 0, 0);
if (pid == -1) {
perror ("waitpid");
return -1;
}
printf ("父进程:发现%u进程退出了。\n", pid);
}
return 0;
}
8)waitpid 函数的 options 参数可取 0 (忽略)或以下值的位或:
WNOHANG - 非阻塞模式,若没有可用的子进程状态,则返回 0。
WUNTRACED - 若支持作业控制,且子进程处于暂停态,则返回其状态。
WCONTINUED - 若支持作业控制,且子进程暂停后继续,则返回其状态。
范例:nohang.c
#include <stdio.h>
#include <errno.h>
#include <sys/wait.h>
int main (void) {
int i;
for (i = 0; i < 3; i++) {
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
return 0;
}
}
for (;;) {
printf ("父进程:我要等待子进程...\n");
pid_t pid = waitpid (-1, 0, WNOHANG);
// 以非阻塞模式等待所有子进程,且不关心终止状态
if (pid == -1) {
if (errno != ECHILD) {
perror ("waitpid");
return -1;
}
printf ("父进程:已经没有子进程可等了,走喽!\n");
break;
}
if (pid) // waitpid 返回非 0,说明子进程退出了;
printf ("父进程:发现%u进程退出了。\n", pid);
else // waitpid 返回 0,说明子进程没退出;
printf ("父进程:没发现子进程退出,干点儿别的...\n");
}
return 0;
}
13 exec
1)exec 函数会用新进程完全替代调用进程,并开始从 main 函数执行。
2)exec 函数并非创建子进程,新进程取调用进程的 PID。
3)exec 函数所创建的新进程,完全取代调用进程的代码段、数据段和堆栈段。
4)exec 函数若执行成功,则不会返回,否则返回 -1。
5)exec 函数包括六种形式:
#include <unistd.h>
int execl (
const char* path,
const char* arg, ...
);
int execv (
const char* path,
char* const argv[]
);
int execle (
const char* path,
const char* arg,
. . .
char* const envp[]
);
int execve (
const char* path,
char* const argv[],
char* const envp[]
);
int execlp (
const char* file,
const char* arg,
. . .
);
int execvp (
const char* file,
char* const argv[]
);
l: 新程序的命令参数以单独字符串指针的形式传入
(const char* arg, ...),参数表以空指针结束。
v: 新程序的命令参数以字符串指针数组的形式传入
(char* const argv[]),数组以空指针结束。
e: 新程序的环境变量以字符串指针数组的形式传入
(char* const envp[]),数组以空指针结束,
无 e 则从调用进程的 environ 变量中复制。
p: 若第一个参数中不包含“/”,则将其视为文件名,
根据 PATH 环境变量搜索该文件。
范例:argenv.c(打印当前的所有命令行参数和环境变量)
#include <stdio.h>
void printarg (int argc, char* argv[]) {
// 第一个参数是命令行参数个数,第二个参数是命令行参数数组
printf ("---- 命令参数 ----\n");
int i;
for (i = 0; i < argc; i++)
printf ("argv[%d] = %s\n", i, argv[i]);
printf ("------------------\n");
}
void printenv (void) {
printf ("---- 环境变量 ----\n");
extern char** environ;
char** env;
for (env = environ; env && *env; env++)
printf ("%s\n", *env);
printf ("------------------\n");
}
int main (int argc, char* argv[]) {
printarg (argc, argv);
printenv ();
return 0;
}
输入:
# ./a.out hello word 1234 55.8
输出:
---- 命令参数 ----
argv[0] = ./a.out
argv[1] = hello
argv[2] = word
argv[3] = 1234
argv[4] = 55.8
---- 环境变量 ----
省略 . . .
-----------------
范例:exec.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
char* path = "./argenv"; // 调用上面的 argenv.c
char* file = "argenv";
char* argv[] = {path, "hello", "world", NULL}; // 参数,必须保证最后以 NULL 结尾;
char* envp[] = {"USER=unknown", "PATH=/tmp", NULL}; // 环境,必须保证最后以 NULL 结尾;
/*
if (execl (path, argv[0], argv[1], argv[2], argv[3]) == -1) {
perror ("execl");
return -1;
}
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
省略 . . .
-----------------
*//*
if (execv (path, argv) == -1) {
perror ("execv");
return -1;
}
输出结果同上
*//*
if (execle (path, argv[0], argv[1], argv[2], argv[3], envp) == -1) {
perror ("execle");
return -1;
}
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
USER=unknown
PATH=/tmp
-----------------
*//*
if (execve (path, argv, envp) == -1) {
perror ("execve");
return -1;
}
输出结果同上
*//*
if (execlp (file, argv[0], argv[1], argv[2], argv[3]) == -1) {
perror ("execlp");
return -1;
}
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
省略 . . .
-----------------
(前提:PATH 必须包含当前路径,否则编译器无法找到 argenv.c,会报错;)
*/
if (execvp (file, argv) == -1) {
perror ("execvp");
return -1;
}
/*
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
省略 . . .
-----------------
(前提:PATH 必须包含当前路径)
*/
return 0;
}
范例:lsexe.c(实现 ls -l)
思路:在 shell 里键入命令,shell 会创建一个子进程来执行命令,主进程调用控制台;
#include <stdio.h>
#include <unistd.h>
int main (void) {
if (execlp ("ls", "ls", "-l", NULL) == -1) {
perror ("execl");
return -1;
}
return 0;
}
范例:fexe.c(实现 ls -l 和 ps -ef 功能)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main (void) {
// 创建子进程,如果不创建,那么只会执行 ls -l,而不会执行 ps -ef
pid_t pid = vfork ();
if (pid == -1) {
perror ("fork");
return -1;
}
// 实现 ls -l
if (pid == 0) {
if (execlp ("ls", "ls", "-l", NULL) == -1) {
perror ("execl");
return -1;
}
return 0;
}
// 回收子进程
int status;
if (waitpid (pid, &status, 0) == -1) {
perror ("waitpid");
return -1;
}
printf ("ls进程以%#x状态退出。\n", WEXITSTATUS (status)); // %#x 十六进制输出
// 实现 ps -ef
if ((pid = vfork ()) == -1) {
perror ("vfork");
return -1;
}
if (pid == 0) {
char* argv[] = {"ps", "-ef", NULL};
if (execvp ("ps", argv) == -1) {
perror ("execv");
return -1;
}
return 0;
}
if (waitpid (pid, &status, 0) == -1) {
perror ("waitpid");
return -1;
}
printf ("ps进程以%#x状态退出。\n", WEXITSTATUS (status));
return 0;
}
14 system
#include <stdlib.h>
int system (const char* command);
1)标准 C 函数;执行 command,成功返回 command 对应进程的终止状态,失败返回 -1。
2)若 command 取 NULL,返回非零表示 shell 可用,返回 0 表示 shell 不可用。
3)该函数的实现,调用了 fork、exec 和 waitpid 等函数,其返回值:
如果调用 fork 或 waitpid 函数出错,则返回 -1。
如果调用 exec 函数出错,则在子进程中执行 exit(127)。
如果都成功,则返回 command 对应进程的终止状态(由 waitpid 的 status 输出参数获得)。
4)使用 system 函数而不用 fork+exec 的好处是,system 函数针对各种错误和信号都做了必要的处理。
范例:system.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main (void) {
int status;
if ((status = system (NULL)) == -1) {
perror ("system");
return -1;
}
if (! status) {
printf ("shell不可用!\n");
return -1;
}
if ((status = system ("ls -l")) == -1) {
perror ("system");
return -1;
}
printf ("ls进程以%#x状态退出。\n", WEXITSTATUS (status));
if ((status = system ("ps -ef")) == -1) {
perror ("system");
return -1;
}
printf ("ps进程以%#x状态退出。\n", WEXITSTATUS (status));
return 0;
}
1) 进程就是运行中的程序。一个运行着的程序,
可能有多个进程。进程在操作系统中执行特定的任务。
2) 程序是存储在磁盘上,包含可执行机器指令和数据的静态实体。
进程或者任务是处于活动状态的计算机程序。
2. 进程的分类
1) 进程一般分为 交互进程、批处理进程 和 守护进程三类。
2) 守护进程总是活跃的,一般是后台运行。
守护进程一般是由系统在开机时通过脚本自动激活启动,或者由超级用户 root 来启动。
3. 查看进程
1) 简单形式
# ps
以简略方式显示当前用户有控制终端的进程信息。
2) BSD 风格常用选项
# ps axu
a - 所有用户有控制终端的进程
x - 包括无控制终端的进程
u - 以详尽方式显示
w - 以更大列宽显示
3) SVR4 风格常用选项
# ps -efl
# ps -efl | grep 10086 // 查看具体某一个进程
# ps -efl | grep a.out
这里 | 是管道符,意思是把前者的输出当作后者的输入;
-e或-A - 所有用户的进程
-a - 当前终端的进程
-u 用户名或用户ID - 特定用户的进程 // #ps -u root
-g 组名或组ID - 特定组的进程 // #ps -g tarena
-f - 按完整格式显示
-F - 按更完整格式显示
-l - 按长格式显示
4) 进程信息列表
USER/UID: 进程属主。
PID: 进程 ID。
%CPU/C: CPU 使用率。
%MEM: 内存使用率。
VSZ: 占用虚拟内存大小(KB)。
RSS: 占用物理内存大小(KB)。
TTY: 终端次设备号,“?”表示无控制终端,如后台进程。
pts/1:虚拟终端,tty3:实体终端;
STAT/S: 进程状态。可取如下值:
O - 就绪。等待被调度。
R - 运行。Linux 下没有 O 状态,就绪状态也用 R 表示。
S - 可唤醒睡眠。系统中断,获得资源,收到信号,都可被唤醒,转入运行状态。
D - 不可唤醒睡眠。只能被 wake_up 系统调用唤醒。
T - 暂停。收到 SIGSTOP 信号转入暂停状态,收到 SIGCONT 信号转入运行状态。
W - 等待内存分页 (2.6 内核以后被废弃)。
X - 死亡。不可见。
Z - 僵尸。已停止运行,但其父进程尚未获取其状态。
< - 高优先级。
N - 低优先级。
L - 有被锁到内存中的分页。实时进程和定制 IO。
s - 会话首进程。
l - 多线程化的进程。
+ - 在前台进程组中。
START/STIME: 进程开始时间。
TIME: 进程运行时间。
COMMAND/CMD: 进程指令。
F: 进程标志。可由下列值取和:
1 - 通过 fork 产生但是没有 exec。
4 - 拥有超级用户特权。
PPID: 父进程 ID。
#ps -f
NI: 进程 nice 值,-20 到 19,可通过系统调用或命令修改。
PRI: 进程优先级。
#ps -lF
静态优先级 = 80 + nice,60 到 99,值越小优先级越高。
内核在静态优先级的基础上,根据进程的交互性计算得到实际(动态)优先级,
以体现对 IO 消耗型进程的奖励,和对处理器消耗型进程的惩罚。
ADDR: 内核进程的内存地址。普通进程显示“-”。
SZ: 占用虚拟内存页数。
WCHAN: 进程正在等待的内核函数或事件。
PSR: 进程被绑定到哪个处理器。
例如:
# ps axu
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2172 668 ? Ss 09:03 0:00 init [5]
root 2 0.0 0.0 0 0 ? S< 09:03 0:00 [migration/0]
root 3 0.0 0.0 0 0 ? SN 09:03 0:00 [ksoftirqd/0]
root 4 0.0 0.0 0 0 tty4 S< 09:03 0:00 [watchdog/0]
root 5 0.0 0.0 0 0 pts/1 S< 09:03 0:00 [events/0]
. . .
# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 500 3223 3217 0 75 0 - 1512 wait pts/1 00:00:00 bash
0 R 500 4561 3223 0 77 0 - 1356 - pts/1 00:00:00 ps
4. 父进程、子进程、孤儿进程和僵尸进程
内核进程(0)
init(1)
xinetd
in.telnetd <- 用户登录
login
bash
vi
1) 父进程启动子进程后,
子进程在操作系统的调度下与其父进程同时运行。
2) 子进程先于父进程结束,
子进程向父进程发送 SIGCHLD(17) 信号,父进程回收子进程的相关资源。
如果父进程没有回收子进程的相关资源,该子进程即成为僵尸进程。
3) 父进程先于子进程结束,子进程成为孤儿进程,
同时被 init 进程收养,即成为 init 进程的子进程。
tips:孤儿进程和僵尸进程不可能同时存在一个进程上;
5. 进程标识符(进程 ID)
1) 每个进程都有一个以非负整数表示的唯一标识,即进程 ID/PID。
2) 进程 ID 在任何时刻都是唯一的,但可以重用,当一个进程退出时,其进程 ID 就可以被其它进程使用。
3) 延迟重用。
范例:delay.c
#include <stdio.h>
int main (void) {
printf ("进程ID:%u\n", getpid ());
return 0;
}
运行测试:
#a.out
进程ID:16431
#a.out
进程ID:16432
#a.out
进程ID:16433
#a.out
进程ID:16431
6. getxxxid
#include <unistd.h>
getpid - 获取进程ID
getppid - 获取父进程ID
getuid - 获取实际用户ID(登录用户 ID)
geteuid - 获取有效用户ID(属主 ID)
getgid - 获取实际组ID
getegid - 获取有效组ID
范例:id.c
#include <stdio.h>
int main (void) {
printf (" 进程ID:%u\n", getpid ());
printf (" 父进程ID:%u\n", getppid ());
printf ("实际用户ID:%u\n", getuid ());
printf ("有效用户ID:%u\n", geteuid ());
printf (" 实际组ID:%u\n", getgid ());
printf (" 有效组ID:%u\n", getegid ());
return 0;
}
以其它用户身份登录并执行
# a.out
输出
进程ID:16443
父进程ID:15890
实际用户ID:1000 - 实际用户 ID 取父进程(shell)的实际用户 ID
有效用户ID:1000 - 有效用户 ID 取实际用户 ID
实际组ID:1000 - 实际组 ID 取父进程(shell)的实际组 ID
有效组ID:1000 - 有效组 ID 取实际组 ID
查看进程 ID:
#ps -eaf | grep 15890
执行
# ls -l a.out
输出
-rwxr-xr-x ...
^ ^
为a.out的文件权限添加设置用户ID位和设置组ID位
# chmod u+s a.out
# chmod g+s a.out
执行
# ls -l a.out
输出
-rwsr-sr-x ...
^ ^
以其它用户身份登录并执行
# a.out
输出
进程ID:...
父进程ID:...
实际用户ID:1000 - 实际用户 ID 取父进程(shell)的实际用户 ID
有效用户ID:0 - 有效用户 ID 取程序文件的属主 ID
实际组ID:1000 - 实际组 ID 取父进程(shell)的实际组 ID
有效组ID:0 - 有效组 ID 取程序文件的属组 ID
进程的访问权限由其有效用户 ID 和有效组 ID 决定。
通过此方法可以使进程获得比登录用户更高的权限。
比如通过 passwd 命令修改登录口令:
执行
ls -l /etc/passwd
输出
-rw-r--r--. 1 root root 1648 Nov 9 14:05 /etc/passwd
^
该文件中存放所有用户的口令信息,仅 root 用户可写,
但事实上任何用户都可以修改自己的登录口令,
即任何用户都可以通过 /usr/bin/passwd 程序写该文件:
执行
# ls -l /usr/bin/passwd
输出
-rwsr-xr-x. 1 root root 28816 Feb 8 2011 /usr/bin/passwd
^ ^
该程序具有设置用户 ID 位,且其属主为 root。
因此以任何用户登录系统,执行 passwd 命令所启动的进程,
其有效用户 ID 均为 root,对 /etc/passwd 文件有写权限。
7. fork
#include <unistd.h>
pid_t fork (void);
1)创建一个子进程,失败返回 -1。
2)调用一次,返回两次;在父进程中返回子进程的 PID;在子进程中返回 0;
利用返回值的不同,可以分别为父/子进程编写不同的处理分支。
范例:fork.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("%u进程:我要调用fork()了...\n", getpid ());
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) { // fork() 在子进程里返回 0;
printf ("%u进程:我是%u进程的子进程。\n", getpid (),
getppid ());
return 0;
}
printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
sleep (1);
return 0;
}
3)子进程是父进程的副本,
子进程获得父进程数据段和堆栈段(包括 I/O 流缓冲区)的拷贝,
但子进程共享父进程的代码段。
范例:mem.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int global = 100;
int main (void) {
int local = 200;
char* heap = (char*)malloc (256 * sizeof (char));
sprintf (heap, "ABC");
printf ("父进程:%d %d %s\n", global, local, heap);
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
// 在子进程里对三个数据作修改
if (pid == 0) {
global++;
local++;
sprintf (heap, "XYZ");
printf ("子进程:%d %d %s\n", global, local, heap);
free (heap);
return 0;
}
sleep (1);
printf ("父进程:%d %d %s\n", global, local, heap);
free (heap);
return 0;
}
输出结果:
父进程:100 200 ABC
子进程:101 201 XYZ
父进程:100 200 ABC
范例:os.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("ABC"); // 放到缓冲区
// 开始分支
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
printf ("XYZ\n");
return 0;
}
sleep (1);
printf ("\n");
return 0;
}
输出:
ABCXYZ // 子进程输出,把父进程的缓冲区复制过来;
ABC // 父进程输出;
范例:is.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("父进程:");
int a, b, c;
scanf ("%d%d%d", &a, &b, &c);
// 开始分支
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
scanf ("%d%d%d", &a, &b, &c);
printf ("子进程:%d %d %d\n", a, b, c);
return 0;
}
sleep (1);
printf ("父进程:%d %d %d\n", a, b, c);
return 0;
}
输入:
父进程:123456
输出:
子进程:456
父进程:123
4)函数调用后父子进程各自继续运行,
其先后顺序不确定,某些实现可以保证子进程先被调度。
5)函数调用后,父进程的文件描述符表(进程级)也会被复制到子进程中,
二者共享同一个文件表(内核级)。
范例:ftab.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void) {
int fd = open ("ftab.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror ("open");
return -1;
}
// 父进程写入一个字符串
const char* text = "Hello, World !"; // 文件指针在 ! 之后那个位置
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
// 进入子进程
pid_t pid = fork ();
if (pid == -1) { // 进入子进程失败
perror ("fork");
return -1;
}
if (pid == 0) { // 进入子进程成功
if (lseek (fd, -7, SEEK_CUR) == -1) { // 从当前位置向文件首移动 7 字节,应该在 W 位置
perror ("lseek");
return -1;
}
close (fd); // 只能关闭子进程的文件描述符,而无法关闭文件表,应为父进程还在用;
return 0;
}
sleep (1);
// 应为文件表共享,所以在子进程里面移动了文件位置,对父进程也有影响
text = "Linux";
if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
perror ("write");
return -1;
}
// 最后 ftab.txt 里面将显示:Hello Linux
close (fd);
return 0;
}
6)总进程数或实际用户 ID 所拥有的进程数,超过系统限制,该函数将失败。
7)一个进程如果希望创建自己的副本并执行同一份代码,
或希望与另一个程序并发地运行,都可以使用该函数。
8)孤儿进程与僵尸进程。
范例:orphan.c(孤儿进程)
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("%u进程:我要调用fork()了...\n", getpid ());
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
sleep (1); // 睡一会,等待父进程死去;
// 验证:如果是孤儿,则 PPID 为 1(init);如果不是,则为上面的 %u 的值;
printf ("\n%u进程:我是被%u进程收养的孤儿进程。", getpid (),
getppid ());
return 0;
}
// 父进程打印完这条语句就死去;
printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
return 0;
}
范例:zombie.c(僵尸进程)
#include <stdio.h>
#include <unistd.h>
int main (void) {
printf ("%u进程:我要调用fork()了...\n", getpid ());
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
// 打完下面这段话,子进程就死去;
printf ("%u进程:我是%u进程的子进程,即将成为僵尸...\n",
getpid (), getppid ());
return 0;
}
// 父进程睡一会,等子进程死去;
sleep (1);
printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
// 此时查看子进程状态
printf ("执行ps -efl | grep %u,按<回车>退出...", pid);
getchar (); // 创造阻塞条件,只有当父进程结束,init 才回收子进程;
return 0;
}
注意:fork 前的代码只有父进程执行,
fork 后的代码父子进程都有机会执行,受代码逻辑的控制而进入不同分支。
8. vfork
#include <unistd.h>
pid_t vfork (void);
该函数的功能与 fork 基本相同,二者的区别:
1)调用 vfork 创建子进程时并不复制父进程的地址空间,
子进程可以通过 exec 函数族,直接启动另一个进程替换自身,进而提高进程创建的效率。
2)vfork 调用之后,子进程先被调度。
用 fork 调用的话,可能是子进程先被调用,也可能是父进程先被调用;
9. 进程的正常退出
从 main 函数中 return(表示整个程序退出)
int main (...) {
. . .
return x;
}
等价于:
int main (...) {
. . .
exit (x);
}
调用标准 C语言的 exit 函数;
在任何函数里调用 exit(); 都会直接退出整个进程;
#include <stdlib.h>
void exit (int status);
1) 调用进程退出,
其父进程调用 wait/waitpid 函数返回 status 的低 8 位。
2) 进程退出之前,
先调用所有事先通过 atexit/on_exit 回调函数注册的函数,
冲刷并关闭所有仍处于打开状态的标准 I/O 流,删除所有通过 tmpfile 函数创建的文件。
#include <stdlib.h>
int atexit (void (*function) (void));
function - 函数指针,指向进程退出前需要被调用的函数。
该函数既没有返回值也没有参数。
成功返回 0,失败返回非零。
int on_exit (void (*function) (int, void*), void* arg);
function - 函数指针,指向进程退出前需要被调用的函数。
该函数没有返回值但有两个参数:
第一参数来自 exit 函数的 status 参数,
第二个参数来自 on_exit 函数的 arg 参数。
arg - 任意指针,将作为第二个参数被传递给 function 所指向的函数。
成功返回 0,失败返回非零。
3) 用 EXIT_SUCCESS/EXIT_FAILURE 常量宏
(可能是 0/1)作参数,调用 exit() 函数表示成功/失败,提高平台兼容性。
4) 该函数不会返回。
因为调完该函数,进程就结束了,无法返回;
5) 该函数的实现调用了 _exit/_Exit 函数。
10. 调用 _exit/_Exit 函数。
#include <unistd.h>
void _exit (int status);
1) 调用进程退出,
其父进程调用 wait/waitpid 函数返回 status 的低 8 位。
2) 进程退出之前,
先关闭所有仍处于打开状态的文件描述符,
将其所有子进程托付给 init 进程(PID 为 1 的进程)收养,向父进程递送 SIGCHILD 信号。
3) 该函数不会返回。
4) 该函数有一个完全等价的标准 C 版本:
#include <stdlib.h>
void _Exit (int status);
进程的最后一个线程执行了返回语句。
进程的最后一个线程调用 pthread_exit 函数。
5)小结:_exit 是系统调用,_Exit 是标 C 函数;
exit 在底层调用 _exit,在调用 _exit 之前会先调用回调函数 atexit/on_exit;
范例:exit.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void doexit1 (void) {
printf ("doexit1()...\n");
}
void doexit2 (int status, void* arg) {
printf ("doexit2(%d,%s)...\n", status, arg);
}
int foo (void) {
printf ("foo()...\n");
// exit (EXIT_SUCCESS);
// exit (EXIT_FAILURE);
exit (1234);
// _exit (1234); 不调用 doexit1 和 doexit2
// _Exit (1234); 不调用 doexit1 和 doexit2
return 10;
}
int main (void) {
if (atexit (doexit1) == -1) {
perror ("atexit");
return -1;
}
if (on_exit (doexit2, "再见") == -1) {
perror ("on_exit");
return -1;
}
printf ("foo()函数返回%d。\n", foo ());
// return 1234; main 函数里面的 return 相当于调用 exit();
}
输出结果如下:
foo()...
doexit2(1234, 再见)...
doexit1()...
分析:主函数执行到 printf ("foo()函数返回%d。\n", foo ()); 时,
调用 foo() 函数,foo() 调用 exit;exit 调用 atexit 和 on_exit;
当 atexit/on_exit 执行完后,程序就结束了;
不会执行 printf ("foo()函数返回%d。\n", foo ()); 里的 "foo()函数返回%d。\n" 语句;
11. 进程的异常终止
1)调用 abort 函数,产生 SIGABRT 信号。
2)进程接收到某些信号。
3)最后一个线程对“取消”请求做出响应。
12. 等待子进程终止并获取其终止状态 wait/waitpid(收尸)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int* status); // 等待所有子进程
pid_t waitpid (pid_t pid, int* status, int options); // 等待具体某一个子进程
成功返回终止子进程的 PID,失败返回 -1。
1)当一个进程正常或异常终止时,内核向其父进程发送 SIGCHLD 信号。
父进程可以忽略该信号,或者提供一个针对该信号的信号处理函数,默认为忽略。
2)父进程调用 wait 函数:
若所有子进程都在运行,则阻塞。
若有一个子进程已终止,则返回该子进程的 PID 和终止状态 (通过 status 参数)。
若没有需要等待子进程,则返回失败 -1,errno 为 ECHILD。
3)在任何一个子进程终止前,wait 函数只能阻塞调用进程,而 waitpid 函数可以有更多选择。
4)如果有一个子进程在 wait 函数被调用之前,
已经终止并处于僵尸状态,wait 函数会立即返回,并取得该子进程的终止状态。
5)子进程的终止状态通过输出参数 status 返回给调用者,若不关心终止状态,可将此参数置空。
6)子进程的终止状态可借助 sys/wait.h 中定义的参数宏查看:
WIFEXITED(): 子进程是否正常终止,是则通过 WEXITSTATUS() 宏,
获取子进程调用 exit/_exit/_Exit 函数,所传递参数的低 8 位。
因此传给 exit/_exit/_Exit 函数的参数最好不要超过 255。
WIFSIGNALED(): 子进程是否异常终止,是则通过 WTERMSIG() 宏获取终止子进程的信号。
WIFSTOPPED(): 子进程是否处于暂停,是则通过 WSTOPSIG() 宏获取暂停子进程的信号。
WIFCONTINUED(): 子进程是否在暂停之后继续运行。
范例:wait.c
#include <stdio.h>
#include <sys/wait.h>
int main (void) {
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
int status = 0x12345678;
printf ("子进程:我是%u进程。我要以%#x状态退出。\n", getpid (), status);
// 输出:子进程:我是 20163 进程。我要以 0x12345678 状态退出。
return status;
}
printf ("父进程:我要等待子进程...\n");
int status;
pid = wait (&status); // wait 会一直等到子进程退出;
printf ("父进程:发现%u进程以%#x状态退出了。\n", pid, WEXITSTATUS (status));
// wait 通过 WEXITSTATUS 得到子进程终止状态;
// WEXITSTATUS 取得所传参数 0x12345678 的低 8 位:0x78
// 输出:父进程:发现 20163 进程以 0x78 状态退出了。
return 0;
}
范例:loop.c
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int main (void) {
int i;
for (i = 0; i < 3; i++) {
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
return 0;
}
}
for (;;) {
printf ("父进程:我要等待子进程...\n");
pid_t pid = wait (0); // 调一次 wait 只能回收一个子进程,所以需要不停循环;
if (pid == -1) { // 返回 -1 有两种情况:子进程都在运行,或者子进程都回收完了;
if (errno != ECHILD) { // 异常退出;
perror ("wait");
return -1;
}
printf ("父进程:已经没有子进程可等了,走喽!\n");
break;
}
printf ("父进程:发现%u进程退出了。\n", pid);
}
return 0;
}
7)如果同时存在多个子进程,又需要等待特定的子进程,可使用 waitpid 函数,其 pid 参数:
pid_t waitpid (pid_t pid, int* status, int options);
-1 - 等待任一子进程,此时与 wait 函数等价。
> 0 - 等待由该参数所标识的特定子进程。
0 - 等待其组 ID 等于调用进程组 ID 的任一子进程,
即等待与调用进程同进程组的任一子进程。
<-1 - 等待其组 ID 等于该参数绝对值的任一子进程,
即等待隶属于特定进程组内的任一子进程。
范例:waitpid.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
pid_t cpid[3];
int i;
for (i = 0; i < sizeof (cpid) / sizeof (cpid[0]); i++) {
cpid[i] = fork ();
if (cpid[i] == -1) {
perror ("fork");
return -1;
}
if (cpid[i] == 0) {
printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
return 0;
}
}
for (i = 0; i < sizeof (cpid) / sizeof (cpid[0]); i++) {
printf ("父进程:我要等待%u进程...\n", cpid[i]);
pid_t pid = waitpid (cpid[i], 0, 0);
if (pid == -1) {
perror ("waitpid");
return -1;
}
printf ("父进程:发现%u进程退出了。\n", pid);
}
return 0;
}
8)waitpid 函数的 options 参数可取 0 (忽略)或以下值的位或:
WNOHANG - 非阻塞模式,若没有可用的子进程状态,则返回 0。
WUNTRACED - 若支持作业控制,且子进程处于暂停态,则返回其状态。
WCONTINUED - 若支持作业控制,且子进程暂停后继续,则返回其状态。
范例:nohang.c
#include <stdio.h>
#include <errno.h>
#include <sys/wait.h>
int main (void) {
int i;
for (i = 0; i < 3; i++) {
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
return 0;
}
}
for (;;) {
printf ("父进程:我要等待子进程...\n");
pid_t pid = waitpid (-1, 0, WNOHANG);
// 以非阻塞模式等待所有子进程,且不关心终止状态
if (pid == -1) {
if (errno != ECHILD) {
perror ("waitpid");
return -1;
}
printf ("父进程:已经没有子进程可等了,走喽!\n");
break;
}
if (pid) // waitpid 返回非 0,说明子进程退出了;
printf ("父进程:发现%u进程退出了。\n", pid);
else // waitpid 返回 0,说明子进程没退出;
printf ("父进程:没发现子进程退出,干点儿别的...\n");
}
return 0;
}
13 exec
1)exec 函数会用新进程完全替代调用进程,并开始从 main 函数执行。
2)exec 函数并非创建子进程,新进程取调用进程的 PID。
3)exec 函数所创建的新进程,完全取代调用进程的代码段、数据段和堆栈段。
4)exec 函数若执行成功,则不会返回,否则返回 -1。
5)exec 函数包括六种形式:
#include <unistd.h>
int execl (
const char* path,
const char* arg, ...
);
int execv (
const char* path,
char* const argv[]
);
int execle (
const char* path,
const char* arg,
. . .
char* const envp[]
);
int execve (
const char* path,
char* const argv[],
char* const envp[]
);
int execlp (
const char* file,
const char* arg,
. . .
);
int execvp (
const char* file,
char* const argv[]
);
l: 新程序的命令参数以单独字符串指针的形式传入
(const char* arg, ...),参数表以空指针结束。
v: 新程序的命令参数以字符串指针数组的形式传入
(char* const argv[]),数组以空指针结束。
e: 新程序的环境变量以字符串指针数组的形式传入
(char* const envp[]),数组以空指针结束,
无 e 则从调用进程的 environ 变量中复制。
p: 若第一个参数中不包含“/”,则将其视为文件名,
根据 PATH 环境变量搜索该文件。
范例:argenv.c(打印当前的所有命令行参数和环境变量)
#include <stdio.h>
void printarg (int argc, char* argv[]) {
// 第一个参数是命令行参数个数,第二个参数是命令行参数数组
printf ("---- 命令参数 ----\n");
int i;
for (i = 0; i < argc; i++)
printf ("argv[%d] = %s\n", i, argv[i]);
printf ("------------------\n");
}
void printenv (void) {
printf ("---- 环境变量 ----\n");
extern char** environ;
char** env;
for (env = environ; env && *env; env++)
printf ("%s\n", *env);
printf ("------------------\n");
}
int main (int argc, char* argv[]) {
printarg (argc, argv);
printenv ();
return 0;
}
输入:
# ./a.out hello word 1234 55.8
输出:
---- 命令参数 ----
argv[0] = ./a.out
argv[1] = hello
argv[2] = word
argv[3] = 1234
argv[4] = 55.8
---- 环境变量 ----
省略 . . .
-----------------
范例:exec.c
#include <stdio.h>
#include <unistd.h>
int main (void) {
char* path = "./argenv"; // 调用上面的 argenv.c
char* file = "argenv";
char* argv[] = {path, "hello", "world", NULL}; // 参数,必须保证最后以 NULL 结尾;
char* envp[] = {"USER=unknown", "PATH=/tmp", NULL}; // 环境,必须保证最后以 NULL 结尾;
/*
if (execl (path, argv[0], argv[1], argv[2], argv[3]) == -1) {
perror ("execl");
return -1;
}
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
省略 . . .
-----------------
*//*
if (execv (path, argv) == -1) {
perror ("execv");
return -1;
}
输出结果同上
*//*
if (execle (path, argv[0], argv[1], argv[2], argv[3], envp) == -1) {
perror ("execle");
return -1;
}
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
USER=unknown
PATH=/tmp
-----------------
*//*
if (execve (path, argv, envp) == -1) {
perror ("execve");
return -1;
}
输出结果同上
*//*
if (execlp (file, argv[0], argv[1], argv[2], argv[3]) == -1) {
perror ("execlp");
return -1;
}
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
省略 . . .
-----------------
(前提:PATH 必须包含当前路径,否则编译器无法找到 argenv.c,会报错;)
*/
if (execvp (file, argv) == -1) {
perror ("execvp");
return -1;
}
/*
输出:
---- 命令参数 ----
argv[0] = ./argenv
argv[1] = hello
argv[2] = word
---- 环境变量 ----
省略 . . .
-----------------
(前提:PATH 必须包含当前路径)
*/
return 0;
}
范例:lsexe.c(实现 ls -l)
思路:在 shell 里键入命令,shell 会创建一个子进程来执行命令,主进程调用控制台;
#include <stdio.h>
#include <unistd.h>
int main (void) {
if (execlp ("ls", "ls", "-l", NULL) == -1) {
perror ("execl");
return -1;
}
return 0;
}
范例:fexe.c(实现 ls -l 和 ps -ef 功能)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main (void) {
// 创建子进程,如果不创建,那么只会执行 ls -l,而不会执行 ps -ef
pid_t pid = vfork ();
if (pid == -1) {
perror ("fork");
return -1;
}
// 实现 ls -l
if (pid == 0) {
if (execlp ("ls", "ls", "-l", NULL) == -1) {
perror ("execl");
return -1;
}
return 0;
}
// 回收子进程
int status;
if (waitpid (pid, &status, 0) == -1) {
perror ("waitpid");
return -1;
}
printf ("ls进程以%#x状态退出。\n", WEXITSTATUS (status)); // %#x 十六进制输出
// 实现 ps -ef
if ((pid = vfork ()) == -1) {
perror ("vfork");
return -1;
}
if (pid == 0) {
char* argv[] = {"ps", "-ef", NULL};
if (execvp ("ps", argv) == -1) {
perror ("execv");
return -1;
}
return 0;
}
if (waitpid (pid, &status, 0) == -1) {
perror ("waitpid");
return -1;
}
printf ("ps进程以%#x状态退出。\n", WEXITSTATUS (status));
return 0;
}
14 system
#include <stdlib.h>
int system (const char* command);
1)标准 C 函数;执行 command,成功返回 command 对应进程的终止状态,失败返回 -1。
2)若 command 取 NULL,返回非零表示 shell 可用,返回 0 表示 shell 不可用。
3)该函数的实现,调用了 fork、exec 和 waitpid 等函数,其返回值:
如果调用 fork 或 waitpid 函数出错,则返回 -1。
如果调用 exec 函数出错,则在子进程中执行 exit(127)。
如果都成功,则返回 command 对应进程的终止状态(由 waitpid 的 status 输出参数获得)。
4)使用 system 函数而不用 fork+exec 的好处是,system 函数针对各种错误和信号都做了必要的处理。
范例:system.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main (void) {
int status;
if ((status = system (NULL)) == -1) {
perror ("system");
return -1;
}
if (! status) {
printf ("shell不可用!\n");
return -1;
}
if ((status = system ("ls -l")) == -1) {
perror ("system");
return -1;
}
printf ("ls进程以%#x状态退出。\n", WEXITSTATUS (status));
if ((status = system ("ps -ef")) == -1) {
perror ("system");
return -1;
}
printf ("ps进程以%#x状态退出。\n", WEXITSTATUS (status));
return 0;
}
相关文章推荐
- shutdown和close的区别
- 如何将std::string转int,double? (C/C++) (C) (template)
- ISO/IEC 9899:2011 条款6.5.3——单目操作符
- 通过HTTP RESTful API 操作elasticsearch搜索数据
- LVM逻辑卷管理
- 致青春
- 英语总结
- 关于thinkphp APP_DEBUG开启后url变成小写的问题
- cvLoadImage()的调用参数设置
- 提高篇(持续更新)
- 怎么设置不让搜索引擎收录某些页面
- IPC是什么意思?
- 菜鸟教程之工具使用(七)——从GIt上导出Maven项目
- NOI库7591 反质数
- Java五个最常用的集合类之间的区别和联系
- 判断avplayer 播放状态
- uc笔记05---sync/fsync/fdatasync,fcntl,文件锁,stat/fstat/lstat
- uc笔记06---chmod/fchmod,chown/fchown/lchown
- Matlab学习第二天 利用插值
- UGUI 代码 动态添加 Event Trigger 的事件