linux编程学习 02 进程与信号
进程
- 程序:是存放在磁盘文件中的可执行文件
进程
- 程序的执行示例被称为进程(process)
- 进程独立的权限和职责,如果系统中某个进程崩溃,它不会影响其余的进程。
- 每个进程允许在气各自的虚拟地址空间中,进程之间可以通过由内核控制的机制互相通讯
进程ID
- 每个linux进程都一定有一个唯一的数字标识符,称为进程ID(process ID),进程ID总是一个非负整数
- 每一个启动的进程都对应一个
task_struct
结构体 task_struct
可以在/usr/src/kernels/2.6.32-642.13.1.el6.x86_64/include/linux/sched.h
文件中查看,注意只有安装了kernel-devel
包才有这个文件
c程序启动过程
- 内核启动特殊例程
启动例程
- 在进程的main函数执行之前内核会启动
- 该历程放置在/lib/libc.so.*中
- 编译器在编译时会将启动例程编译进可执行文件中
启动例程作用
- 搜集命令行的参数传递给main函数中的argc和argv
- 搜集环境信息构建环境表并传递给main函数
- 登记进程的终止函数
进程终止
正常终止
- 从main函数返回
- 调用exit(标准c库函数)
- 调用
_exit
或_Exit
(系统调用) - 最后一个线程从其启动例程返回
- 最后一个线程调用pthread_exit
异常终止
- 调用abort
- 接受到一个信号并终止
- 最后一个线程对取消请求做处理响应
进程返回
- 通常程序运行成功返回0,否则返回非0
- 在shell中可以查看进程返回值(echo $?)
atexit函数
int atexit(void (*function)(void));
- 返回:若成功则为0,若出错则为-1
- 功能:像内核登记终止函数
- 头文件:
#include <stdlib.h>
- 每个启动的进程都默认登记了一个标准的终止函数
- 终止函数在进程终止时释放进程所占用的一些资源
- 登记的多个终止函数执行顺序是以栈的方式执行,先登记的后执行
- 使用_exit或_Exit退出时,不会执行终止函数
进程终止方式的区别
对比项 | return | exit() | _exit()/_Exit() |
---|---|---|---|
是否刷新标准I/O缓存 | 是 | 是 | 否 |
是否自动调用终止函数 | 是 | 是 | 否 |
在centos 6.6 中,使用 _exit和_Exit会调用终止函数
ps命令
- 可以查看到:进程的id,进程的用户id,进程状态和进程的command
ps aux
可以查看到更多的信息- ps输出信息
|列名|解释|
USER | 进程的属主 |
PID | 进程的ID |
PPID | 父进程ID |
%CPU | 进程占用的CPU时间 |
%MEM | 占用内存的百分比 |
NI | 进程的NICE值,数值大,表示较少占用CPU时间 |
VSZ | 进程的虚拟大小 |
RSS | 驻留集的大小,可以理解为内存中页的数量 |
TTY | 终端ID |
WCHAN | 正在等待的进程资源 |
START | 启动进程的时间 |
TIME | 进程消耗CPU的时间 |
COMMAND | 命令的名称和参数 |
进程状态
进程常见状态
运行状态
- 系统当前进程
- 就绪状态进程
- ps命令的STAT列为R
等待状态
- 等待事件发生
- 等待系统资源
- 等待状态可分为可中断等待和不可中断等待
- 可中断等待时ps命令的STAT列为S
- 不可中断等待时ps命令的STAT列为D
停止状态
- ps命令的STAT列为T
僵尸状态
- 进程终止或结束
- 在进程表项中仍有记录
- ps命令的STAT列为Z
进程调度
- 第一步:处理内核中的工作
- 第二步:处理当前进程
第三步:选择进程
- 实时进程
- 普通进程
- 第四步:进程交换
task_struct中的调度信息
策略
- 轮流策略
- 先进先出策略
优先权
- Jiffies变量
实时优先权
- 实时进程之间
- 计数器
进程标识
pid_t getpid(void);
获得当前进程IDpid_t getuid(void);
获得当前进程的实际用户IDpid_t geteuid(void);
获得当前进程的有效用户ID,启动程序时使用的用户id,参照nginxpid_t getgid(void);
获得当前进程的用户组IDpid_t getppid(void);
获得当前进程的父进程IDpid_t getpgrp(void);
获得当前进程所在进程组IDpid_t getpgid(pid_t pid);
获得进程ID为pid的进程所在的进程组ID
- 头文件
#include <unistd.h>
#include <sys/types.h>
进程创建
pid_t fork(void);
pid_t vfork(void);
- 返回:子进程中为0,父进程中为子进程ID,出错为-1
- fork创建的新进程被称为子进程,该函数被调用一次,但返回两次。两次返回的区别是:在子进程中的返回值是0,而在父进程中的返回值则是进进程的进程ID
- 创建子进程,父子进程哪个先运行根据系统调度且复制父进程的内存空间
- vfork创建子进程,但子进程先运行且不复制父进程的内存空间
子进程的继承
子进程的继承属性
- 用户的信息和权限,目录信息,信号信息,环境,共享存储段,资源限制,堆,栈和数据段,就是把父进程的信息复制一遍。但是共享代码段(复制虚拟地址,但是虚拟地址映射到同一个物理地址)。
子进程特有属性
- 进程ID,锁信息,运行时间,未决信号
操作文件时的内核结构变化
- 子进程只继承父进程的文件描述表,不继承但共享文件表项和i-node。
- 父进程创建一个子进程后,文件表项的引用计数器加1变2,当父进程作close操作后,计数器减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项
- 实例
/**
* 如何创建子进程
* */
void fork_example1(void)
{
int pid = fork();
if(pid == -1)
{
printf("fork error\n");
}
// 根据fork的返回值判断是父进程还是子进程
if(pid > 0)
{
// 父进程睡眠下,如果父进程在子进程结束之前结束,子进程ppid就为1
sleep(1);
printf("I am parent process,my pid is %d,my ppid is %d\n",getpid(),getppid());
}
else
{
printf("I am child process,my pid is %d,my ppid is %d\n",getpid(),getppid());
}
}
/**
* 父子进程哪个先运行由系统调度
**/
void fork_example2(void)
{
int pid = fork(),l = 10;
if(pid == 0)
{
for(int i = 0;i < l;i++)
{
sleep(1);
printf("parent pid => %d,ppid => %d;count => %d\n",getpid(),getppid(),i);
}
}
else
{
for(int i = 0;i < l;i++)
{
sleep(1);
printf("child pid => %d,ppid => %d;count => %d\n",getpid(),getppid(),i);
}
}
}
int a = 1;
/**
* 子进程和父进程的虚拟地址是一样的,下面的打印可以看出,但是物理地址是不一样的
* */
void fork_example3(void)
{
int b = 2;
int pid = fork();
if(pid > 0)
{
printf("&a = %p;&b = %p",&a,&b);
}
else if(pid == 0)
{
printf("&a = %p;&b = %p",&a,&b);
}
}
/**
* io
* */
void fork_example4(void)
{
FILE* f1 = fopen("f1.txt","w+");
int f2 = open("f2.txt",O_CREAT | O_WRONLY);
// c标准函数写,这里的start在文件里面会写入两次,因为子进程会把标准的id的缓存也复制一边>,最后结束时,就会写入两次
fprintf(f1,"start\n");
// 系统函数写
write(f2,"start\n",6);
int pid = fork();
if(pid > 0)
{
// c标准函数写
fprintf(f1,"parent end\n");
// 系统函数写
write(f2,"parent end\n",11);
}
else if(pid == 0)
{
// c标准函数写
fprintf(f1,"child end\n");
// 系统函数写
write(f2,"child end\n",10);
}
fclose(f1);
close(f2);
}
void fork_example5(void)
{
int f = open("a.txt",O_WRONLY | O_CREAT);
int pid = fork();
if(pid > 0)
{
// 主进程调整位置,子进程写入
lseek(f,0,SEEK_END);
}
else
{
sleep(3);
// f是主进程f的复制,所以主进程使用lseek对子进程也是有效的
write(f,"write",5);
}
close(f);
}
进程链
int main(void)
{
int pid = 0;
for(int i = 10;i > 0;i--)
{
if(pid > 0)
{
sleep(i);
printf("pid = %d;ppid = %d\n",getpid(),getppid());
break;
}
else if(pid == 0)
{
pid = fork();
}
}
return 0;
}
进程扇
int main(void)
{
int pid = 0;
for(int i = 10;i > 0;i--)
{
pid = fork();
if(pid == 0)
{
printf("pid = %d;ppid = %d\n",getpid(),getppid());
return 0;
}
}
sleep(3);
printf("pid = %d;ppid = %d\n",getpid(),getppid());
return 0;
}
守护进程
- 守护进程(deamon)是生存期长的一种进程。他们常常在系统引导装入时启动,在系统关闭时终止。
- 所有守护进程都以超级用户(用户id为0)的优先权运行
- 守护进程没有控制终端
- 守护进程的父进程都是init进程
孤儿进程
- 父进程结束,子进程就成为孤儿进程,会由1号进程(init进程)领养。
僵尸进程
- 子进程结束但是没有完全释放内存(在内核中的task_struct没有释放),该进程就成为僵尸进程。
- 当僵尸进程的父进程结束后就会被init进程领养。最终被回收
避免僵尸进程
- 让僵尸进程的父进程来回收,父进程每隔一段时间来查询子进程是否结束并回收,调用wait()或waitpid(),通知内存释放僵尸进程
- 采用信号SIGCHLD通知处理,并在信号处理程序中调用wait函数
- 让僵尸进程成为孤儿进程,有init回收
- 实例
int main(void)
{
int pid = fork();
if(pid == 0)
{
printf("child finish\n");
// 这里return之后子进程就成为僵尸进程了
return 0;
}
while(1)
{
sleep(1);
}
return 0;
}
wait和waitpid
pid_t wait(int* status);
- 返回:成功返回子进程id,出错返回-1
- 功能:等待子进程推出并回收,防止僵尸进程的产生
pid_t waitpid(pid_t pid,int* status,int options);
- 返回:成功返回子进程id,出错返回-1
- 功能:wait函数的非阻塞版本
- 头文件
#include <sys/types.h>
#include <sys/wait.h>
wait与waitpid函数区别
- 在一个子进程终止前,wait使其调用者阻塞
- waitpid有一选择项,可使调用者不阻塞
- waitpid等待一个指定的子进程,而wait则等待所有的子进程,返回任一终止子进程的状态
status参数
- 为空时,代表任意状态结束的子进程,若不为空,则代表指定状态结束的子进程
检查wait和waitpi函数返回终止状态的宏
WIFEXITED/WEXITSTATUS(status)
- 若为正常终止子进程返回的状态,则为真
WIFSIGNALED/WTERMSIG(status)
- 若为异常终止子进程返回的状态则为真(接到一个不能捕捉的信号)
WIFSTOPPED/WSTOPSIG(status)
- 若为当前暂停子进程的返回的状态,则为真
options参数
WNOHANG
- 若由pid指定的进程没有退出则立即返回,则waaitpid不阻塞,此时其返回值为0
WUNTRACED
- 若某实现支持作业控制,则由pid指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态
waitpid函数的pid参数
pid == -1
- 等待任一子进程,功能与wait等效
pid > 0
- 等待进程id与pid相等的子进程
pid == 0
- 等待其组id等于调用进程的组id的任一子进程
pid < -1
- 等待其组id等于pid的绝对值得任一进程
- 示例
void out_put(int status)
{
int i = 0;
if(WIFEXITED(status))
{
printf("normal exit;");
i = WEXITSTATUS(status);
}
else if(WIFSIGNALED(status))
{
printf("abnormal exit;");
i = WTERMSIG(status);
}
else if(WIFSTOPPED(status))
{
printf("stopped sig;");
i = WSTOPSIG(status);
}
printf("status = %d\n",i);
}
int main(void)
{
int pid = fork();
int status;
if(pid == 0)
{
printf("pid = %d;ppid = %d\n",getpid(),getppid());
sleep(3);
// 暂停
pause();
exit(1);
return 11;
}
// 会阻塞
// wait(&status);
// sleep(3);
// waitpid(pid,&status,WNOHANG);
// 不会阻塞,
while(0 == waitpid(pid,&status,WUNTRACED | WNOHANG))
{
}
out_put(status);
return 0;
}
exec函数
- 在用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序
- 当进程调用一冲exec函数时,该程序完全由新程序代换,替换原有进程的正文,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程id并不改变。exec只是用另一个新程序替换了当前进程的正文,数据,堆和栈。
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* pathname,const char* arg0,.../* (char*)0 */);
int execvp(const char* pathname,char* const argv[]);
- 返回:出错返回-1,成功则不返回
exec系列函数注意点
- execve函数为系统调用,其余为库函数。执行execve函数后面的代码不执行
- execlp和execvp函数中的pathname,相对路径和绝对路径均可使用,其他四个函数中的pathname只能使用绝对路径。相对路径一定要在进程环境表对应的PATH中
- argv参数为新程序执行main函数中传递argv参数,最后一个元素为NULL
- envp为进程的环境表
六个函数都是以“exec”四个字母开头,后面的字幕代表了其用法上的区别:
- 带有字母的“l”函数,表明后面的参数列表是要传递给程序的参数列表,参数列表的第一个参数必须是执行程序,最后一个参数必须是NULL
- 带有字母“p”的函数,第一参数可以是相对路径或程序名,如果无法立即找到要执行的程序,那么就在环境变量PATH指定的路径中搜索。其他函数的第一个参数则必须是绝对路径名。
- 带有字母“v”的函数,表明程序的参数列表通过一个字符串数组来传递。这个数组和最后传递给程序的main函数的字符串数组argv完全一样。第一个参数必须是程序名。最后一个参数也必须是NULL
- 带有字母“e”的函数,用户可以自己设置程序接收一个设置环境变量的数组
- 示例
int main(void)
{
char command1[] = "cat",command2[] = "/bin/cat";
char *parameters[20] = {command1,"/etc/passwd",NULL};
int pid = fork();
if(pid == 0)
{
// 注意传递参数时,命令也要传递进去
if(execl(command2,parameters[0],parameters[1],parameters[2]))
// if(execvp(command1,parameters))
{
perror("error\n");
return 1;
}
else
{
// 下面这个不会输出,因为子进程成功不会返回
printf("success\n");
return 0;
}
}
wait(NULL);
return 0;
}
system函数
int systemp(const char* command);
- 返回:成功返回命令执行的状态,出错返回-1
- 功能:简化exec函数的使用
- 头文件
#include <stdlib.h>
- system函数内部构建一个子进程,由子进程调用exec函数
- 等同于
/bin/bash -c "command"
或者exec("bash","-c","cmd")
- 示例
int main(void)
{
char* command = "date";
// system(command);
my_system(command);
return 0;
}
void my_system(char* command)
{
int pid = fork();
if(pid == 0)
{
execl("/bin/bash","bash","-c",command,NULL);
perror("error:");
}
wait(NULL);
}
进程状态的切换
new ---fork---> runnable <-os scheduler/timeout-> running -return/exit/_exit-> dead
↑ |
| read/write/sleep/pause
| ↓
---------------------block suspend
信号
- 信号signal机制是linux系统中最古老的进程之间的通信机制,解决进程在正常运行中被中断的问题,导致进程的处理流程会发生变化
- 信号是软件中断
信号是异步事件
- 不可预见
- 信号有自己的名称和编号
- 信号和异常处理机制
信号发生的来源
- 硬件来源:比如我们按下了键盘或其他硬件故障,信号是由硬件驱动程序产生
- 软件来源:最常用发送信号的系统函数是kill(),raise(),alarm()和setitimer()等函数,软件来源还包括一些非法运算登操作,软件设置条件(如:gdb调试),信号是由内核产生。
信号无优先级
1~31:非实时信号,发送的信号可能会丢失,不支持信号排队
31~64:实时信号,支持信号排队发送的多个实时信号都会被接收
可在/usr/include/bits/signum.h
信号的处理
进程可以通过三种方式来响应和处理一个信号
忽略信号
- SIGKILL和SIGSTOP永远不能忽略
- 忽略硬件异常
- 进程启动时SIGUSR1和SIGUSR2两个信号被忽略
执行默认操作
- 每个信号有默认动作大部分信号默认动作是终止进程
捕获信号
- 告诉内核出现信号时调用自己的处理函数
- SIGKILL和SIGSTOP不能被捕获
信号变革
- 信号出现在最早的unix系统中
- 早期信号模型是不可靠
- BSD和System V分别对早期信号进行扩展,但是相互不兼容
- POSIX统一了上述两种模型,提供了可靠信号模型
SIGNAL函数
void (*signal(int signo,void (*func)(int)))(int);
- 返回:若成功则返回先前的信号处理函数指针,出错则返回SIG_ERR
- 功能:向内核登记信号处理函数
参数
signo
- 要登记的信号值,一般使用参数的宏,方便辨识
func
- 信号处理函数指针
- SIG_IGN:忽略信号
- SIG_DFL:采用系统默认方式处理信号,执行默认操作
- 示例
int main(void)
{
// kill -20 pid即可看到函数被执行了
// signal(SIGTSTP,my_signal_handle);
// 忽略
if(signal(SIGTSTP,SIG_IGN) == SIG_ERR)
{
perror("error");
}
for(int i = 0;i < 6;i++)
{
sleep(6);
printf("for => i = %d\n",i);
}
return 0;
}
void my_signal_handle(int sig)
{
printf("I am my signal handle\n;my pid is %d\nsignal number is %d\n",getpid(),sig);
}
SIGCHLD信号
- 子进程状态发生变化(子进程结束)产生该信号,父进程需要使用wait调用来等待子进程结束并回收它
- 避免僵尸进程
- 示例
void my_sigchld_handle(int sig)
{
// 当接受到信号时要调用wait函数,否则子进程会是僵尸进程
wait(NULL);
printf("excute wait;pid = %d\n",getpid());
}
int main(void)
{
// 子进程结束时会执行
signal(SIGCHLD,my_sigchld_handle);
int pid = fork();
if(pid > 0)
{
for(int i = 0;i < 66;i++)
{
sleep(1);
printf("I am parent;pid = %d;count = %d\n",getpid(),i);
}
}
else if( pid == 0 )
{
for(int i = 0;i < 6;i++)
{
sleep(1);
printf("I am child;pid = %d;count = %d\n",getpid(),i);
}
}
else
{
perror("fork error");
}
return 0;
}
信号发送
- 除了内核和超级用户,并不是每个进程都可以向其他的进程发送信号。
- 一般的进程只能向具有相同uid和gid的进程发送信号,或向相同进程组中的其他进程发送信号
- 常用的发送信号的函数有kill(向其他进程发送),raise(向自己发送),alarm(定时器),settimer(定时器),abort(异常终止信号)
kill和raise函数
`int kill(pid_t pid,int signo);
- 返回:成功返回0,出错返回-1
- 功能:向指定的进程发送某一个信号
`int raise(int signo); - 返回:成功返回0,出错返回-1
- 功能:向进程本身发送某一个信号
- 头文件
#include <signal.c>
参数:
pid:接受信号进程的pid
- pid>0:将信号发送给进程id为pid的进程
- pid==0:将信号发送给与发送进程同一进程组的所有进程
- pid<0:将信号发送给进程组id为pid的绝对值的所有进程
- pid==-1:将该信号发送给发送进程有权限像他们发送信号的系统上的所有进程
- signo:要发送的信号值
kill函数将信号发送给函数或进程组
- 0为空信号,常用来检测特定进程是否存在
- 示例
int main(void)
{
signal(SIGUSR1,my_signal_handle);
for(int i = 0;i < 66;i++)
{
// 程序直接结束
// raise(SIGUSR1);
kill(getpid(),SIGSTOP);
}
return 0;
}
void my_signal_handle(int signo)
{
printf("I am signal handle\n");
}
定时器
unsigned int alarm(unsigned int seconds);
- 返回:0或以前设置的定时器时间余留秒数
- 头文件
#include <unistd.h>
- alarm函数可设置定时器,当定时器超时,产生SIGALRM
- 信号由内核产生,在指定的seconds秒之后,给进程本身发送一个SIGALRM信号
- 参数为0,取消以前设置的定时器。
- 一次性的
- ualarm函数精确性更高
- 示例
int main(void)
{
signal(SIGALRM,my_sigalarm_handle);
// 六秒后发送SIGALRM信号到本进程
alarm(6);
for(int i = 0;i < 66;i++)
{
printf("i = %d\n",i);
sleep(1);
}
}
void my_sigalarm_handle(int signo)
{
printf("sigalrm signo = %d\n",signo);
// 每隔五秒发送一次信号
alarm(6);
// 信号注册函数也需要重新执行下
signal(SIGALRM,my_sigalarm_handle);
}