linux进程编程入门
1、进程的创建与操作
任务描述:
- 在父进程中创建一个全局变量,一个局部变量,并赋予初始值,用fork函数创建子进程。在子进程中对父进程的变量进行自加操作,并且输出变量值,然后父进程睡眠一段时间
- 各进程结束前输出进程与父进程号,全局及局部变量值
相关知识:
由 fork 创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0,而父进程的返回值则是新子进程的进程 ID。将子进程 ID 返回给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以获得其所有子进程的进程 ID。fork 使子进程得到返回值0 的理由是:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID (进程 ID 0 总是由交换进程使用,所以一个子进程的进程 ID 不可能为 0 )。子进程和父进程共享很多资源,除了打开文件之外,很多父进程的其他性质也由子进程继承
main.c:
#include<stdio.h> #include<unistd.h> #include<sys/types.h> int a=1; int main(void) { int b=2; pid_t pid; pid=fork(); if(pid==0){ printf("PID is %d,",getpid()); printf("PPID is %d\n",getppid()); a++;b++; printf("a=%d,b=%d\n",a,b);
exit(0); } else{ sleep(1); printf("after fork\n"); printf("PID is %d,",getpid()); printf("PPID is %d\n",getppid()); printf("a=%d,b=%d\n",a,b); } sleep(5); return 0; }
2、vfork函数的使用
任务描述:
- 在父进程中创建一个局部变量,并赋予初始值0
- 采用fork函数创建子进程,调用wait函数等待子进程结束,在父进程中输出局部变量值
- 通过vfork函数创建另一个子进程,调用wait函数等待子进程结束,在父进程中输出局部变量值
相关知识:
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程。
vfork 用于创建新进程,而该新进程的目的是exec一个新程序(执行一个可执行的文件)。新进程有自己的地址空间,因此 vfork 函数并不将父进程的地址空间完全复制到子进程中。
在使用 vfork()时,必须在子进程中调用 exit()函数调用,否则会出现:_new_exitfn:Assertion `l != ((void *)0)' failed
wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用 wait()时子进程已经结束,则 wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数 status 返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数 status 可以设成 NULL。
main.c:
#include<stdio.h> #include<unistd.h> #include<sys/types.h> int main(void) { int b=0,i; pid_t pid; printf("fork choose 0;vfork choose 1\n"); scanf("%d",&i); switch(i){ case 0:pid=fork();break; case 1:pid=vfork();break; default:printf("error\n"); } if(pid==0){ b++; exit(0); } wait(pid); printf("data is %d\n",b); return 0; }
3、子进程与父进程执行的顺序
任务描述:
- 用fork函数创建子进程,子进程创建一个for循环,每次sleep 1s再打印输出子进程的pid和自加变量i的值
- 父进程sleep 3s,再打印输出父进程的pid,以及自加变量i的值
main.c:
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<stdlib.h> int main(void) { int i; pid_t pid; pid=fork(); if(pid==0){ for(i=1;i<6;i++){ sleep(1); printf("process ID %d,i=%d\n",getpid(),i); } exit(0); } else if(pid > 0 ){ sleep(3); printf("PPID is %d,i=%d\n",getpid(),i); }else{ perror("fork error\n"); exit(-1); } printf("hello"); wait(pid); // system("ps -o pid,ppid,state,tty,command"); return 0; }
4、vfork
任务描述:
- 用vfork函数创建子进程,子进程创建一个for循环,每次sleep 1s再打印输出子进程的pid和自加变量i的值
- 父进程sleep 3s,再打印输出父进程的pid,以及自加变量i的值
main.c:
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<stdlib.h> int main(void) { int i; pid_t pid; pid=vfork(); if(pid==0){ for(i=1;i<10;i++){ sleep(1); printf("process ID %d,i=%d\n",getpid(),i); } exit(0); } else{ sleep(3); printf("PPID is %d,i=%d\n",getpid(),i); wait(pid); // system("ps -o pid,ppid,state,tty,command"); } return 0; }
运行程序可以看出使用vfork是先执行子进程再执行父进程的.
5、clone函数
任务描述:
- 主进程创建一个全局变量variable,并赋予初始值9,打印输出当前variable值
- 用clone函数创建子进程,clone的标志为CLONE_VM,CLONE_FILES,在子进程的函数do_something中将变量variable值修改为42
- 在主进程中sleep 3s,再打印输出variable值
相关知识:
clone 函数功能强大,带了众多参数,因此由他创建的进程要比前面 2 种方法要复杂。clone 可以让你有选择性的继承父进程的资源,你可以选择想 vfork 一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。函数的结构int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);这里 fn 是函数指针,我们知道进程的 4 要素,这个就是指向程序的指针,就是所谓的“剧本", child_stack 明显是为子进程分配系统堆栈空间(在 linux 下系统堆栈空间是 2页面,就是 8K 的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块 task_struct 的值),flags 就是标志用来描述你需要从父进程继承那些资源, arg 就是传给子进程的参数)。下面是 flags 可以取的值:
CLONE_PARENT | 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” |
CLONE_FS | 子进程与父进程共享相同的文件系统,包括 root、当前目录、umask |
CLONE_FILES | 子进程与父进程共享相同的文件描述符(file descriptor)表 |
CLONE_NEWNS | 在新的 namespace 启动子进程,namespace 描述了进程的文件hierarchy |
CLONE_SIGHAND | 子进程与父进程共享相同的信号处理(signal handler)表 |
CLONE_PTRACE | 若父进程被 trace,子进程也被 trace |
CLONE_VFORK | 父进程被挂起,直至子进程释放虚拟内存资源 |
CLONE_VM | 子进程与父进程运行于相同的内存空间 |
CLONE_PID | 子进程在创建时 PID 与父进程一致 |
CLONE_THREAD | Linux 2.4 中增加以支持 POSIX 线程标准,子进程与父进程共享相同的线程群 |
main.c:
#define _GNU_SOURCE #define childstack 1000 #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sched.h> int variable=9; void *stack; int fn() { variable=42; free(stack); exit(1); } int main(void) { void *stack; stack=malloc(childstack); printf("The variable was %d\n",variable); clone(&fn,stack + childstack,CLONE_VM|CLONE_FILES,NULL); sleep(3); printf("in child process\n"); printf("The variable is now %d\n",variable); return 0; }
6、WIFEXIED宏
任务描述:
- 子进程输出pid后,用exit函数结束,参数为3
- 父进程调用wait收集子进程退出状态
- 调用宏WIFEXITED(status)来查看子进程是否为正常输出
- 在主进程中sleep 3s ,再打印输出variable值
- 当WIFEXITED返回非0值时,用宏WEXITSTATUS来提取子进程的返回值
相关知识:
进程一旦调用了 wait,就立即阻塞自己,由 wait 自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait 就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait 就会一直阻塞在这里,直到有一个出现为止。
参数 status 用来保存被收集进程退出时的一些状态,它是一个指向 int 类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为 NULL,就象下面这样:
pid = wait(NULL);
如果成功,wait 会返回被收集的子进程的进程 ID,如果调用进程没有子进程,调用就会失败,此时 wait 返回-1,同时 errno 被置为 ECHILD。
参数 status:
如果参数 status 的值不是 NULL,wait 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完
成这项工作:
①WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值(请注意,虽然名字一样,这里的参数 status 并不同于 wait 唯一的参数---指向整数的指针 status,而是那个指针所指向的整数,切记不要搞混了)
②WEXITSTATUS(status) 当 WIFEXITED 返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用 exit(5),WEXITSTATUS(status) 就会返回 5;如果子进程调用 exit(7),WEXITSTATUS(status)就会返回 7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED 返回 0,这个值就毫无意义。
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { pid_t pid; pid=fork(); int status; if(pid==0){ printf("This is child process with pid of %d\n",getpid()); exit(3); } else{ pid=wait(&status); sleep(3); if(WIFEXITED(status)!=0) printf("the child process %d exit normally\n",pid); if(WEXITSTATUS(status)==3) printf("the return code is %d\n",WEXITSTATUS(status)); } return 0; }
7、waitpid函数
任务描述:
- 子进程sleep 10s,父进程非阻塞等待子进程结束
- 收到子进程结束消息后结束进程,否则给予输出提示未收到子进程结束消息
相关知识:
pid_t waitpid(pid_t pid,int *status,int options)
pid:
从参数的名字 pid 和类型 pid_t 中就可以看出,这里需要的是一个进程 ID。但当pid 取不同的值时,在这里有不同的意义。pid>0 时,只等待进程 ID 等于 pid 的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid 就会一直等下去。
pid=-1 时,等待任何一个子进程退出,没有任何限制,此时 waitpid 和 wait 的作用一模一样。
pid=0 时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会对它做任何理睬。
pid<-1 时,等待一个指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
options:
options 提供了一些额外的选项来控制 waitpid,目前在 Linux 中只支持WNOHANG 和 WUNTRACED 两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);如果我们不想使用它们,也可以把 options 设为 0,如:ret=waitpid(-1,NULL,0);如果使用了 WNOHANG 参数调用 waitpid,即使没有子进程退出,它也会立即返回,不会像 wait 那样永远等下去。
返回值和错误
当正常返回的时候,waitpid 返回收集到的子进程的进程 ID;
如果设置了选项 WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0;
如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在;
当 pid 所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid 就会出错返回,这时 errno 被设置为 ECHILD;
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/wait.h> int main(void) { int pr; pid_t pid; pid=fork(); if(pid==0){ printf("This is child, pid=%d. Sleeping...\n",getpid()); sleep(10); exit(1); } else{ do{ pr=waitpid(pid,NULL,WNOHANG);//没有子进程退出,它也会立即返回,不会像 wait 那样永远等下去 if(pr==0){ printf("No child exited\n"); sleep(1); } }while(pr==0); if(pid==pr) printf("successfully get child %d\n",pid); } return 0; }
8、_exit函数
任务描述:
- 第一个程序用printf函数先后输出两句话,用exit(0)结束进程
- 第二个程序输出同第一个程序,用_exit(0)结束进程
相关知识:
①exit 系统调用带有一个整数类型的参数 status,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0 表示没有意外的正常结束;其他的数值表示出现了错误,进程非正常结束。我们在实际编程时,可以用 wait 系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。
②exit 和_exit 作为系统调用而言,_exit 和 exit 是一对孪生兄弟。这种区别主要体现在它们在函数库中的定义。_exit 在 Linux 函数库中的原型是:
#include<unistd.h>
void _exit(int status);
exit 比较一下,exit()函数定义在 stdlib.h 中,而_exit()定义在 unistd.h 中,从名字上看,stdlib.h 似乎比 unistd.h 高级一点,那么,它们之间到底有什么区别呢?
_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为 exit 已经不能算是纯粹的系统调用. exit()函数与_exit()函数最大的区别就在于 exit()函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是“清理 I/O 缓冲”.在Linux 的标准函数库中,有一套称作“高级 I/O”的函数,我们熟知的 printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(buffered I/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n 和文件结束符 EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用 exit()函数。
main1.c:
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> int main(void) { printf("hello\n"); printf("fuck"); exit(0); return 0; }
main2.c:
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main(void) { printf("hello\n"); printf("fuck"); _exit(0); return 0; }
9、exec函数族的使用
任务描述:
- 通过exec函数族的6个函数列出当前文件夹下的所有目录和文件
- execl、execlp、execle、execv、execvp、execve
相关知识:
①exec 函数族说明
fork()函数用于创建一个子进程,该子进程几乎复制了父进程的全部内容,但是,这个新创建的进程如何执行呢?exec 函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux 下任何可执行的脚本文件。
在 Linux 中使用 exec 函数族主要有两种情况:
● 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用 exec 函数族中的任意一个函数让自己重生。
● 如果一个进程想执行另一个程序,那么它就可以调用 fork()函数新建一个进程,然后调用 exec 函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。
②exec 函数族语法
实际上,在 Linux 中并没有 exec()函数,而是有 6 个以 exec 开头的函数,它们之间的语法有细微差别,本书在后面会详细讲解。
exec 函数族成员函数语法:
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[])
这 6 个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式、参数传递方式及环境变量这几个方面进行比较。
● 查找方式。读者可以注意到,表 2 中的前 4 个函数的查找方式都是完整的文件目录路径,而最后两个函数(也就是以 p 结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量“$PATH”所指定的路径进行查找。
● 参数传递方式。exec 函数族的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第 5 位字母来区分的,字母为“l”(list)的表示逐个列举参数的方式,其语法为 const char *arg;字母为“v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为 char*const argv[]。读者可以观察 execl()、execle()、execlp()的语法与 execv()、execve()、execvp()的区别,它们的具体用法在后面的实例讲解中会具体说明。这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以 NULL 结束。
● 环境变量。exec 函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数 execle()和 execve()就可以在 envp[]中指定当前进程所使用的环境变量。
事实上,这 6 个函数中真正的系统调用只有 execve(),其他 5 个都是库函数,它们最终都会调用 execve()这个系统调用。在使用 exec 函数族时,一定要加上错误判断语句。exec 很容易执行失败,其中最常见的原因有:
● 找不到文件或路径,此时 errno 被设置为 ENOENT。
● 数组 argv 和 envp 忘记用 NULL 结束,此时 errno 被设置为 EFAUL。
● 没有对应可执行文件的运行权限,此时 errno 被设置为 EACCES。
main.c:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> int main(int argc, char *argv[]) { char *env[] = {"/bin/ls","ls", "-a", NULL}; pid_t pid; pid=fork(); if( fork() == 0 ){ printf("1------------execl------------\n" ); if( execl( "/bin/ls", "ls","-a", NULL ) == -1 ){ perror( "execl error " ); exit(1); } } wait(pid); if( fork() == 0 ){ printf("2------------execlp------------\n"); if( execlp( "ls", "ls", "-a", NULL ) < 0 ){ perror( "execlp error " ); exit(1); } } wait(pid); if( fork() == 0 ){ printf("3------------execle------------\n"); if( execle("/bin/ls", "ls", "-a", NULL, NULL) == -1 ){ perror("execle error "); exit(1); } } wait(pid); return 0; }
10、exec函数族的使用
任务描述:
- 修改task9的代码,调用所给程序,打印输出所有命令行参数和所有环境字符串
相关知识:
每个程序都有一个环境变量表,和命令行参数表一样,环境变量表也是一个指针数组。
extern char ** environ
echoall.c:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int argc, char *argv[]) { int i; char **ptr; extern char **environ; for(i=0;i<argc;i++) printf("argv[%d]: %s\n", i, argv[i]); for(ptr=environ; *ptr!=0; ptr++) printf("%s\n",*ptr); exit(0); }
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<errno.h> #include<string.h> int main(int argc,char**argv,char**env) { execle("./echoall","./echoall", "myarg1", "MY ARG2",NULL,env); fprintf(stderr,"%s\n",strerror(errno)); return 0; }
11、exec函数族使用
任务描述:
- 创建hello.c用于输出环境变量
- 父进程中创建两个环境变量AA=11,BB=22
- 子进程输出父进程传递过来的环境变量
hello.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main(int argc,char *argv[]) { int i; char **ptr; extern char **environ; for(ptr = environ; *ptr !=0; ptr ++) printf("%s\n",*ptr); exit(0); }
main.c:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { char *ps_envp[]={"AA=11","BB=22",NULL}; pid_t pid; pid=fork(); if(pid<0){ printf("error!\n"); return -1; } if(pid==0){ printf("Entering main ...\n"); printf("hello pid=%d\n",getpid()); if(execle("./hello","hello",NULL,ps_envp)==-1){ printf("error\n"); exit(0); } }
ptr=wait(NULL); return 0; }
12、僵死进程
任务描述:
- 编写一个僵死进程
- 用system命令执行ps命令验证该进程是僵死进程
相关知识:
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
产生原因:任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。父进程尚未对已终止子进程调用 wait 函数善后,尚留存进程 ID、终止状态等信息,只有等到父进程处理或父进程结束被 init 领养才能释放资源。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
waitpid 返回终止子进程的进程 ID。并将该子进程的终止状态存放在有 status 指向的存储单元中。
#include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t pid; pid=fork(); if(pid==0){ printf("create a zoombie\n"); exit(0); } printf("pid is %d\n",getpid()); // system("ps a"); system("ps -o pid,ppid,state,tty,command"); return 0; }
13、 僵死进程的避免
任务描述:
- 调用fork函数两次以避免僵死进程
#include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t pid; pid=fork(); if(pid==0) { printf("create a son\n"); pid=fork(); if(pid>0) exit(0); printf("create a grandson\n"); sleep(2); printf("parent pid= %d\n",getppid()); exit(0); } return 0; }