11 进程和信号

11.2 进程的结构

init进程PID为1,是系统启动后的第一个进程,是所有进程的祖先进程。

Linux进程不能对用来存放程序代码的内存区域进行写操作,即程序代码是以只读的方式加载到内存中的。一些代码,如系统库函数,可以被多个进程共享。

进程有自己的栈空间,用来保存函数中的局部变量和控制函数的调用与返回。进程还有自己的环境空间,包含当前进程的环境变量。

在目录系统中,/proc下的一些特殊文件,以文件的形式记录了进程的信息。

ps命令

默认情况下,ps命令只会显示与终端、主控台等保持连接的进程的信息。如果要查看所有进程的信息,用ps -a

ps ax可以看到STAT一列,表示进程的状态,含义如下:

  • S:睡眠

  • R:运行,严格来说是处于运行队列之中的进程

  • D:不可中断的睡眠,通常是在等待输入或输出的完成

  • T:停止。通常是被shell作业控制所停止,或进程处于调试器的控制之下

  • Z:defunct或zombie进程

  • N:低优先级,nice

  • <:高优先级

  • s:进程时会话期首进程

  • +:前台进程

  • l:多线程进程

使用nice命令可以手动调整进程的nice值。

11.3 启动新的进程

system启动shell

system函数用一个shell来启动想要执行的程序,参数就是想要执行的命令的字符串

#include <stdlib.h>
#include <stdio.h>
int main()
{
printf("Running ps with system\n");
system("ps ax"); // 在头文件stdlib中
printf("Done.\n");
exit(0);
}
leo@ubuntu:~/c_test$ ./a.out
Running ps with execlp
PID TTY STAT TIME COMMAND
1 ? Ss 0:05 /sbin/init auto noprompt
.....
2918 pts/1 R+ 0:00 ps ax
Done.

exec替换进程映像

exec总共有6个相关函数

#include <unistd.h>
char **environ;
int execl(const char *path, const char *arg0, ..., (char *) 0);
int execlp(const char *file, const char *arg0, ..., (char *) 0);
int execle(const char *path, const char *arg0, ..., (char *) 0, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

前三个函数的参数个数是可变的,参数以一个空指针结束。

execlp和execvp通过搜索环境变量来查找新程序的可执行文件的路径。execle和execve则通过最后一个env字符是字符串数组来作为环境变量。

全局变量environ可用来把一个值传递给新的程序环境中。

下面给出这6个函数的使用例子

#include <unistd.h>
/* Example of an argument list */
/* Note that we need a program name for argv[0] */
char *const ps_argv[] = {"ps", "ax", 0};
/* Example environment, not terribly useful */
char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", 0};
/* Possible calls to exec functions */
execl("/bin/ps", "ps", "ax", 0);
execlp("ps", "ps", "ax", 0); /* assumes /bin is in PATH */
execle("/bin/ps", "ps", "ax", 0, ps_envp); /* passes own environment */
execv("/bin/ps", ps_argv);
execvp("ps", ps_argv);
execve("/bin/ps", ps_argv, ps_envp);
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Running ps with execlp\n");
execlp("ps", "ps", "ax", 0);
printf("Done.\n");
exit(0);
}

执行结果最后没有输出Done.,这是因为原有的进程已经被替换了。

leo@ubuntu:~/c_test$ ./a.out
Running ps with system
PID TTY STAT TIME COMMAND
1 ? Ss 0:05 /sbin/init auto noprompt
.....
2918 pts/1 R+ 0:00 ps ax
leo@ubuntu:~/c_test$

fork复制进程映像

fork将会基于原有进程创建一个新进程。新进程几乎和原进程一样,执行的代码也相同。但是新进程有自己的数据空间、环境和文件描述符。

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

fork在父进程中返回子进程的pid,在子进程中返回0,返回-1表示出错。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
char *message;
int n;
printf("fork program starting\n");
pid = fork();
switch (pid) {
case -1:
perror("fork failed");
exit(1);
case 0:
message = "This is the child";
n = 5;
break;
default:
message = "This is the parent";
n = 3;
break;
}
for (; n > 0; n--) {
puts(message);
sleep(1);
}
exit(0);
}

可以看到child输出了5次,parent只输出了3次。

leo@ubuntu:~/c_test$ ./a.out
fork program starting
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
leo@ubuntu:~/c_test$ This is the child
This is the child

wait等待一个进程

在父进程中可以调用wait函数让父进程等待子进程的结束。函数返回子进程的pid,参数状态信息允许父进程了解子进程的状态,即子进程main函数的返回值或exit函数的退出码。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
char *message;
int n;
int exit_code;
printf("fork program starting\n");
pid = fork();
switch (pid) {
case -1:
perror("fork failed");
exit(1);
case 0:
message = "This is the child";
n = 5;
exit_code = 37;
break;
default:
message = "This is the parent";
n = 3;
exit_code = 0;
break;
}
for (; n > 0; n--) {
puts(message);
sleep(1);
}
/* 这部分代码只有在父进程中会执行,等待子进程结束后,输出提示信息 */
if (pid != 0) {
int stat_val;
pid_t child_pid;
child_pid = wait(&stat_val);
printf("Child has finished: PID = %d\n", child_pid);
if (WIFEXITED(stat_val))
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
else
printf("Child terminated abnormally\n");
}
/* 子进程的退出码是37 */
exit(exit_code);
}
leo@ubuntu:~/c_test$ ./a.out
fork program starting
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the child
This is the child
Child has finished: PID = 2974
Child exited with code 37

对于wait参数的状态信息,在<sys/wait.h>中有一些宏来解释

说明

WIFEXITED(stat_val)

如果子进程正常结束,它就取一个非0值

WEXITSTATUS(stat_val)

如果WIFEXITED非0,它就返回子进程的退出码

WIFSIGNALED(stat_val)

如果子进程因为一个未捕获的信号而终止,它就去一个非0值

WTERMSIG(stat_val)

如果WIFSIGNALED非0,它就返回一个信号代码

WIFSTOPPED(stat_val)

如果子进程以外终止,它就取一个非0值

WSTOPSIG(stat_val)

如果WIFSTOPPED非0,它就返回一个信号代码

要等待特定pid的子进程,可以用函数waitpid

#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *stat_loc, int options);

参数pid如果为-1,则返回任意子进程的信息。options是个非常有用的参数。如果我们既想check子进程,有不想让父进程被block,则可以用WNOHANG选项,如果子进程没有结束,父进程会继续执行。

僵尸进程

当子进程终止时,其与父进程的关联还会保持,进程表中子进程的数据还在。直到父进程也正常终止或者调用wait。

子进程运行结束后,父进程结束前(或者调用wait前)这段时间的子进程被称为僵尸进程。

如果此时父进程异常终止,那么子进程的父进程会变成init进程。init进程会清理僵尸进程。

示例:字符串换程序

这个程序把一个文件中的所有英文字母都转换成大写。

首先是一个

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
int main()
{
int ch;
while ((ch = getchar()) != EOF) {
putchar(toupper(ch));
}
exit(0);
}

上面的程序可以这样使用

leo@ubuntu:~/c_test$ ./upper < file.txt
I AM LEO
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *filename;
if (argc != 2) {
fprintf(stderr, "usage: useupper file\n");
exit(1);
}
/* freopen会先关闭标准输入 */
filename = argv[1];
if (!freopen(filename, "r", stdin)) {
fprintf(stderr, "could not redirect stdin from file %s\n", filename);
exit(2);
}
/* 这里执行upper进程替换当前进程 */
execl("./upper", "upper", 0);
/* 如果没有正确执行,这里才会被执行到 */
perror("could not exec. ./upper");
exit(3);
}

线程概述

Linux中用多线程比进程麻烦,也不常见。Linux中的进程都是非常轻量级的,而且容易编写。

11.4 信号

信号概述

  • raise(生成):表示一个信号的产生

  • catch(捕获):表示接收到一个信号

信号的名称在signal.h中定义,用kill -l可以查看所有信号

leo@ubuntu:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

如果进程接受到信号,但是没有安排捕获,会立即停止。通常我们敲中断字符(ctrl+c)会像前台进程(当前正在运行的程序)发送SIGINT信号。

kill命令

kill命令可以给指定PID进程发送信号,kill -HUP 512就是给PID给512的进程发HUP信号(挂断信号)。

用killall可以给所有运行某一命令的进程发送信号,如killall -HUP inetd

signal库函数

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

signal有两个参数,sig是准备捕获或忽略的信号,第二个参数是处理信号的函数。信号处理函数必须有一个int类型的参数(即接收到的信号的代码),且返回类型是void。

第二个参数也可以用两个有特殊含义的值类代替信号处理函数

  • SIG_IGN:忽略信号

  • SIG_DFL:恢复默认行为

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig)
{
printf("OUCH! - I got signal %d\n", sig);
(void) signal(SIGINT, SIG_DFL);
}
int main()
{
(void) signal(SIGINT, ouch);
while (1) {
printf("Hello World!\n");
sleep(1);
}
}
leo@ubuntu:~/c_test$ ./a.out
Hello World!
Hello World!
Hello World!
^COUCH! - I got signal 2
Hello World!
Hello World!
Hello World!
^C

发送信号

kill函数和shell中的kill命令具有相同的功能

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

普通用户只能发送信号给自己的进程。超级用户可以发送信号给任何进程。

#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int alarm_fired = 0;
void ding(int sig)
{
alarm_fired = 1;
}
int main()
{
pid_t pid;
printf("alarm application starting\n");
pid = fork();
switch (pid) {
case -1:
perror("fork failed");
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/* in parent process */
printf("waiting for alarm to go off\n");
(void) signal(SIGALRM, ding);
pause(); // pause()把现有进程挂起,直到有信号来为止
if (alarm_fired)
printf("Ding!\n");
printf("Done!");
exit(0);
}

输出

leo@ubuntu:~/c_test$ ./a.out
alarm application starting
waiting for alarm to go off
Ding!

未完