赞
踩
写在前面:注意,本章除了讲解进程管理,还包含网络编程Socket API的知识。
进程用 进程ID 做唯一的标识,叫PID,函数getpid()可以获取当前进程的pid。
查看进程的方式:
1、Windows用ctrl + alt + delete启动任务管理器;
2、Linux/Unix用ps命令查看进程。
ps 只能显示当前终端启动的进程。
ps -aux : Linux专用选项,Unix不直接支持(/usr/usb/ps可以用)。
ps -ef : Unix/Linux通用的选项
如果进程比较多,可以用管道实现分页,命令如下:
ps -aux | more
实现空格翻页、回车翻行、q退出。
ps命令可以查看进程的如下信息:
进程PID、进程的启动者(属主)、CPU和内存使用率、状态、
父进程的PID、启动的程序是哪个
其中,进程的状态主要状态包括:
S - 休眠状态,进程大多数处于休眠状态
s - 说明该进程有子进程(父进程)
R - 正在运行的进程
Z - 僵尸进程(已经结束但资源没有回收的进程)
关于父进程和子进程
操作系统中的多进程是有启动的次序的,Unix系统先启动0进程,0进程再启动进程1和进程2(有些系统只启动进程1),然后0进程就休眠。进程1和进程2启动其他进程,其他进程再启动其他的进程,直到所有的进程都启动为止。
如果进程a启动了进程b,a叫b的父进程,b叫a的子进程。
进程用PID表示,PID是一个非负的正数,PID可以延迟重用。因此PID在同一
时刻保证唯一。
几个常用的函数:
getpid() - 取当前进程的PID
getppid() - 取当前进程的父进程的PID
getupid() - 取当前用户的ID。
创建子进程的方法:
方法一: fork() 创建子进程,通过复制父进程创建子进程。因此父进程对应相同的代码区。
方法二:vfork() + execl()创建子进程,父进程和子进程的代码区完全不同,父子进程执行
的是完全不同的代码。
fork()是一个非常复杂的简单函数:
pid_t fork();
返回子进程的PID或者0,失败返回-1.没有参数。
fork()是通过复制父进程的内存空间创建子进程,复制除了代码区之外的所有区域,
代码区父子进程共享(只读)。
fork()创建一个子进程,子进程从fork()当前位置开始执行,fork()之前的代码父进程
执行一次,fork()之后的代码父子进程分别执行一次(共2次)
fork()函数自身会返回两次,父进程返回子进程的PID,子进程会返回0.注意了是函数的返回。
fork()创建子进程时,会复制除了代码区之外的所有区域,包括缓冲区。
fork()创建子进程时,如果父进程有文件描述符,子进程会复制文件描述符,不复制
文件表(父子进程共用一个文件表)。
父子进程的关系:
fork()创建子进程后,父子进程同时运行,如果子进程先结束,子进程会给父进程发信号,父进程回收子进程的资源。
fork()创建子进程后,父子进程同时运行,如果父进程先结束,子进程会变成孤儿进程,认进程1(init进程)做新的父进程。init进程也叫孤儿院。
fork()创建子进程后,父子进程同时运行,如果子结束时父进程没有收到信号或没有及时处理,子进程将变成僵尸进程。
具体fork()原理参考下图:
例子一:
fork1.c
#include <stdio.h> #include <unistd.h> int main() { printf("begin\n"); pid_t pid = fork(); printf("end%d\n",pid); } //执行结果: begin end11710 end0 如果begin没有加\n #include <stdio.h> #include <unistd.h> int main() { printf("begin");//begin在输出缓冲区,子进程复制 pid_t pid = fork();//缓冲区,而不是执行第5行 printf("end%d\n",pid); } //执行结果: beginend11710 beginend0
fork2.c
#include <stdio.h>
#include <unistd.h>
int main() {
//父子进程使用不同的分支
pid_t pid = fork();
if(!pid){
//父子进程都有,但子进程符合条件
printf("我是子进程\n");
}
else{
//父子进程都有,父进程执行,子进程不进行
printf("我是父进程\n");
}
}
例子二:要求在父子进程中,分别打印出父子进程的PID
//格式:我是子进程1234,我是父进程1233
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if(!pid){
//父子进程都有,但子进程符合条件
printf("我是子进程%d,父进程是%d\n",getpid(),getppid());
//getpid()获得当前pid,getppid()获得当前的父进程pid
}
else{
//父子进程都有,父进程执行,子进程不进行
printf("我是父进程%d,子进程是%d\n",getpid(),pid);
}
}
fork3.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int i1 = 10; int main() { int i2 = 10; int* pi = malloc(4); *pi = 10; pid_t pid = fork(); if(!pid) { //子进程执行的分支 i1 = 20; i2 = 20; *pi = 20; printf("child:i1=%d,i2=%d,*pi=%d\n",i1,i2,*pi); //打印结果是i1=20 i2=20 *pi=20 printf("child:i1=%p,i2=%p,*pi=%p\n",&i1,&i2,pi); //fork()创建的子进程会复制父进程的虚拟内存地址,但映射到 //不同的物理内存上,同时把原来的值拷贝过来。复制完成后父进 //程的内存就独立了。 exit(0);//结束子进程 } sleep(1); //父进程 printf("father:i1=%d,i2=%d,*pi=%d\n",i1,i2,*pi); //打印结果是i1=10 i2=10 *pi=10 //为什么是10,可以参考上面的图,改变子进程的栈区堆区全局 //区,父进程的是不变的 printf("father:i1=%p,i2=%p,*pi=%p\n",&i1,&i2,pi); //打印的虚拟内存地址和子进程一样 }
练习三:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("a.txt",O_RDWR|O_CREAT|O_TRUNC,0666); if(fd == -1)perror("open"),exit(-1); pid_t pid = fork();//有两个fd,子进程将复制fd if(pid == 0){ //子进程执行的分支 write(fd,"hello",5);//只复制描述符,不复制文件表 close(fd);//关闭子进程的fd exit(0); } write(fd,"12345",5); close(fd);//关闭父进程的fd return 0; } //运行结果是12345hello(也就是说父和子进程没有相互覆盖, 因为不复制文件表,共用一个文件表,只有一个文件偏移量)
如果代码改成下面的,先fork(),在open"a.txt",则会出现hello和12345的覆盖
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> int main() { pid_t pid = fork();//先fork()的话,没有复制,而是创建 int fd = open("a.txt",O_RDWR|O_CREAT|O_TRUNC,0666); if(fd == -1)perror("open"),exit(-1); if(pid == 0){ //子进程执行的分支 write(fd,"hello",5);//只复制描述符,不复制文件表 close(fd);//关闭子进程的fd exit(0); } write(fd,"12345",5); close(fd);//关闭父进程的fd return 0; }
fork()与open()关系如下图:
例子四:接下来这个程序验证父和子进程是否同时进行:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if(!pid) { //子进程的分支 printf("子进程是%d,父进程是%d\n",getpid(),getppid()); //getpid()获得当前pid sleep(2);//休眠2秒 printf("子进程是%d,父进程是%d\n",getpid(),getppid()); exit(0); } sleep(1);//导致父进程先结束 printf("pid = %d\n",getpid());//父进程pid return 0; } 执行结果: lioker:Desktop$ gcc 1.c lioker:Desktop$ ./a.out 子进程是12760,父进程是12759 pid = 12759 lioker:Desktop$ 子进程是12760,父进程是1862 //因为父进程先结束,所以子进程重新认定一个父进程
接下来讲进程的退出
正常退出:
1在main()中执行return语句
2执行exit()函数
3执行_Exit()或_exit()函数
4最后一个线程退出
5主线程退出
非正常退出:
1被信号打断导致退出
2最后一个线程被取消
今天研究exit()、_exit()和_Exit()都是用来退出进程的,区别在于:
_exit和_Exit()是一样的,区别在于头文件不同(uc/标c)
exit()和_Exit()区别主要在于退出的方式不同:
_Exit()是立即退出,exit()不是立即退出,还可以调用一些其他函数后再退出。
可以使用atexit()函数注册一些函数,这个函数在exit()之前会被自动调用,return也会调用。
补充一下函数指针:
void fa(void);//函数声明
void (*fa)(void);//函数指针(fa是函数名)
以下为exit()、atexit()函数程序代码:
#include <stdio.h>
#include <stdlib.h>
void fa(void){
printf("fa called\n");
}
int main() {
atexit(fa);//注册退出前的函数fa,现在不调用
printf("begin\n");
exit(0);
printf("end\n");
}//运行结果为begin -> fa call -> end
改为下面的:
#include <stdio.h>
#include <stdlib.h>
void fa(void){
printf("fa called\n");
}
int main() {
atexit(fa);//注册退出前的函数fa,现在不调用
printf("begin\n");
_exit(0);
printf("end\n");
}//运行结果为begin
再改为下面的:
#include <stdio.h>
#include <stdlib.h>
void fa(void){
printf("fa called\n");
}
int main() {
atexit(fa);//注册退出前的函数fa,现在不调用
printf("begin\n");
printf("end\n");
}//运行结果为begin -> end -> fa call。这是因为系统自动在
//主函数最后加了return 0;
wait()和waitpid()
wait和waitpid()可以让父进程等待子进程的结束,并取得子进程的退出状态和退出码(return后面的值或exit()括号中的值)
wait()和waitpid()的区别在于wait()很固定,而waitpid()更灵活。
wait()是等待任意一个子进程结束后返回,而waitpid()可以选择等待的子进程,也可以不等待。
wait()等待的子进程包括僵尸子进程,因此wait()也叫殓尸工。
pid_t wait(int* status)
参数是一个传出参数(指针),用来带出结束子进程的退出码和退出状态;
关于返回,有结束子进程就返回他的PID,没有就等待,父进程自己阻塞,如果出了错就返回-1。
宏函数WIFEXITED(status)可以判断是否正常退出,
WEXITSTATUS(status)可以取到退出码。
代码如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if(!pid) { //子进程的分支 sleep(2); printf("子进程%d即将结束\n",getpid()); exit(100);//进程正常退出,并且完成了功能 //exit(-1),负数,表示进程正常退出,但没有完成任务 } int status; pid_t wpid = wait(&status);//如果子进程不结束,父进程则阻塞等待子进程结束 printf("等待到了%d的退出\n",wpid); if(WIFEXITED(status)){ //说明正常退出了 printf("正常退出,退出码:%d\n",WEXITSTATUS(status)); } } //运行结果: lioker:Desktop$ gcc 1.c lioker:Desktop$ ./a.out 子进程13436即将结束 等待到了13436的退出 正常退出,退出码:100
waitpid()有更多的选择
pid_t waitpid(pid_t pid,int *status,int option);
参数ststus和wait()一样,pid可以等待哪些或哪个子进程,option可以设定是否等待。
pid的值可能是:
== -1 等待任意子进程,与wait()等效; >0 等待指定子进程(指定pid);
==0 等待本进程组的任一子进程; <-1 等待进程组ID等于pid绝对值的任一子进程
注:后两个有时候用不到,了解即可
option的值:0 阻塞,父进程等待;WNOHANG 不阻塞,直接返回0
关于返回值:有子进程结束时返回子进程的pid,出错返回-1
如果为阻塞方式,没有子进程结束则继续等待;
如果是WNOHANG,若子进程不立即可用,则不阻塞,返回0。
代码如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if(pid == -1)perror("fork"),exit(-1); if(!pid) { //子进程的分支 sleep(1); printf("子进程%d即将结束\n",getpid()); exit(100); } pid_t pie2 = fork(); if(!pid2){ sleep(3); printf("子进程%d即将结束\n",getpid()); exit(200); } int status; pid_t wpid = waitpid(pid2,&status,0);//如果waitpid()里的pid2改成-1即 //waitpid(-1,&status,0)则pid和pid2 //谁先结束选谁 if(WIFEXITED(status)){ printf("等到了%d子进程,退出码:%d\n",wpid,WEXITSTATUS(status)); } }
接下来讲vfork() + execl()方式创建子进程:
vfork()和fork()在语法上没有区别,唯一的区别在于vfork()不复制父进程的任何资源,而是直接占用父进程的资源运行代码,父进程处于阻塞状态,直到子进程结束或者调用了exec系列函数(比如:execl())。
vfork()和execl()的合作方式:
vfork()可以创建新的进程,但没有代码和数据,execl()创建不了进程,但可以为进程提供代码和数据。
和fork()不同,vfork()创建子进程后确保子进程先运行,父进程到子进程调用到了execl()之后才能运行。
注:vfork()如果占用的是父进程的资源,必须用exit()显式退出。
代码如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(){ pid_t pid = vfork(); if(!pid){ printf("子进程%d开始运行\n",getpid()); sleep(3); printf("子进程%d结束\n",getpid()); exit(0);//vfork()占用父进程资源,必须用exit()退出 } printf("父进程结束\n"); return 0; }
execl()是exec系统函数中的第一个,功能是启动一个全新的进程,替换当前的进程。启动的这个进程会全面覆盖旧进程,但不会新建进程。(会替换各种区域,但pid不变)
execl("程序的路径","执行的命令","参数",NULL);
只有第一个参数是必须正确的,第二个参数必须存在但可以不正确,第三个和第四个参数可以没有,NULL代表参数结束了。
比如运行我们的程序:
execl("./b.out","b.out",NULL);
代码如下:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("begin\n");
execl("./bin/ls","ls","-l",NULL);
printf("end\n");
}
//运行结果:
begin
total 16
-rw------- 1 lioker lioker 131 4月 21 17:45 1.c
-rwxrwxr-x 1 lioker lioker 8648 4月 21 17:45 a.out
这是因为execl()覆盖掉了旧进程,所以printf("end\n");未执行
下面把execl()与vfork()函数结合使用
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(){ pid_t pid = vfork(); if(!pid){ execl("/bin/ls","ls","-l",NULL); printf("child\n");//打印不出来,不执行 exit(0);//有意义,针对execl()出错时,退出子进程 } printf("父进程开始运行\n"); sleep(1); } //运行结果: 父进程开始运行 total 16 -rw------- 1 lioker lioker 212 4月 21 18:04 1.c -rwxrwxr-x 1 lioker lioker 8800 4月 21 18:05 a.out
代码:两个.c文件
proc.c
#include <stdio.h>
int main(){
printf("pid=%d\n",getpid());
}
vfork3.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(){ pid_t pid = vfork(); if(!pid){ printf("pid=%d\n",getpid); execl("./proc","proc",NULL); exit(0);//有意义,针对execl()出错时,退出子进程 } printf("父进程开始运行\n"); sleep(1); } //执行结果: pid=10583 父进程开始运行 pid=10583 也就是说,两个pid一样。这是因为execl()的pid不会改变
IPC - 进程间通信(InterProcess Communication, IPC)
Unix系统早期都是多进程解决问题的,因此多个进程之间需要交互数据,而进程间不能直接交互数据,IPC就是解决这个问题的。(后来就又出现了多线程技术)。
IPC主要包括:
1、文件
2、信号
3、管道
4、共享内存
5、消息队列
6、信号量集(与信号没有任何的关系)
7、网络socket
其中,共享内存、消息队列和信号量集都是XSI IPC,遵循相同的规范。因此编程有很多共性的地方。
而管道是古老的IPC,目前很少使用。
下面是IPC原理图:
信号是Unix/Linux系统下最常见的一种软件中断方式。
中断就是让程序停止当前正在运行的代码,转而执行其他代码的过程。
中断分为软件中断和硬件中断,软件中断就是用软件的方式中断代码。
信号导致代码中断的案例很多,比如:
ctrl C ->信号2
kill -9 -> 信号9
段错误 -> 信号11
总线错误 -> 信号7(不确定,不同系统不一定)
Unix系统信号从1到48,Linux系统从1-64,但不确定连续,而且规范中没有规定信号的数量。
信号都有一个宏名称,以SIG开头,比如:信号2叫SIGINT,宏名称的本质就是一个非负整数,查看信号的命令:
kill -l
注:编程时,信号使用宏名称,因为有些系统信号数字不同,但宏名称是一样的。
常见的一些宏名称:
SIGINT - 信号2 ctrl+C
SIGQUIT - 信号3 ctrl+
SIGKILL - 信号9
… …
在Linux系统中,1-31是不可靠信号,是早期的信号,不支持排队,所以有可能丢失;34-64是可靠信号,支持排队,不可能丢失。信号分为可靠信号和不可靠信号。(可以比喻成,饭店生意太火,如果有地方支持排队吃饭,就是可靠。饭店太小不支持排队,顾客就走了丢失了)。
当信号是不可靠信号时进程只处理一个,当信号是可靠信号时,进程则依据排队一个一个处理。
信号的处理方式:信号只是一个整数,实现中断的功能依靠信号处理。信号的处理方式有三种:
1默认处理,是系统提供的,多半是退出进程。
2忽略信号,信号来了不做额外处理,直接忽略。
3自定义处理函数,信号按程序员的代码处理。
注:有些信号是不能被自定义和忽略了,比如信号9直接将进程强制退出;
进程可以给其他进程发信号,但只能给本用户的进程发信号,root可以给所有的用户进程发信号。
提前说明:kill()函数和kill命令的作用是向进程发送信号;而signal()函数的作用是接收信号并把信号传递给特定的函数中,就是说得有外界发送一个信号之后(比如按键盘ctrl+C)signal()才能收到信号然后起作用。
signal()/sigaction()可以设置信号的处理方式
signal()使用了一个函数指针,原型:
void (*f)(int)
函数指针 signal(int 信号值,函数指针)
其中,第二个参数函数指针可以是以下三个值:
SIG_IGN - 忽略该信号
SIG_DFL - 恢复默认处理
传入一个程序员自定义函数名 - 自定义处理函数
返回,成功返回之前的处理方式,失败返回SIG_ERR。
注:signal()函数只是设定了信号处理方式,自身并没有发信号,因此信号处理函数在信号到来时才执行。
kill命令用于发信号,格式:
kill -信号 进程pid
信号0没有实际的意义,用于测试是否有发信号的权限。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(){
printf("pid=%d\n",getpid());
while(1);
}
//在一个终端运行:
lioker:Desktop$ ./a.out
pid=15428
再打开一个终端:kill -0 15428则1无反应
kill -11 15428则段错误Segmentation fault (core dumped)
#include <stdio.h> #include <stdlib.h> #include <signal.h> void fa(int signo){ //系统会自动把信号的值传递给参数 printf("捕获了信号%d\n",signo); } int main(){ signal(SIGINT,fa);//信号2交给fa if(signal(SIGQUIT,SIG_IGN) == SIG_ERR)//达到把信号3忽略的目的 perror("signal 3"),exit(-1); signal(9,fa);//这里信号9调signal()没作用 printf("pid=%d\n",getpid()); while(1); } //执行结果: pid=15547 ^C捕获了信号2 ^C捕获了信号2 ^\^\^\^\^\^\ Killed 就是说,在另一个终端执行kill -9 15547,这个进程才被杀死。
注意了,关于常用的信号的用法:
1、头文件<signal.h>
2、fa这个函数
3、signal(SIGINT,fa);signal(SIGQUIT,SIG_IGN) ;
改成下面的:
#include <stdio.h> #include <stdlib.h> #include <signal.h> void fa(int signo){ //系统会自动把信号的值传递给参数 printf("捕获了信号%d\n",signo); signal(signo,SIG_DFL);//第一次给fa,第二次恢复默认 } int main(){ signal(SIGINT,fa);//信号2交给fa if(signal(SIGQUIT,SIG_IGN) == SIG_ERR)//达到把信号3忽略的目的 perror("signal 3"),exit(-1); signal(9,fa);//这里信号9调signal()没作用 printf("pid=%d\n",getpid()); while(1); } //执行结果: pid=15547 ^C捕获了信号2 在输入一次^C就终止了,因为恢复默认了
父子进程之间的信号处理:
如果父进程用fork()创建的子进程,子进程的与父进程的信号处理方式完全一致(照抄)。如果父进程用vfork()+execl()方式创建的子进程,父进程自定义处理函数的子进程改为默认(因为子进程已经没有了处理函数),其他照抄。
代码验证一下:
fork.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){
printf("捕获了信号%d\n",signo);
}
int main(){
signal(SIGINT,fa);//信号2交给了fa
signal(SIGQUIT,SIG_IGN)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。