zoukankan      html  css  js  c++  java
  • Linux多进程编程

    进程的状态

    Linux进程有7种基础状态(两种running算一种),除了traced都可以用$ps命令查看,$ps可以查看的进程状态如下,更多进程状态信息参见Linux Process VS Thread VS LWP
    R running or runnable (on run queue)
    D uninterruptible sleep (usually IO)
    S interruptible sleep (waiting for an event to complete)
    T stopped, either by a job control signal or because it is being traced.
    W paging (not valid since the 2.6.xx kernel)
    X dead (should never be seen)
    Z defunct ("zombie") process, terminated but not reaped by its parent.

    模型

    多进程代码区模型(其他区参见copy-on-write):

    #include <sys/types.h> 
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    getpid()/getuid()/getgid()					//获得PID/UID/GID
    fork()/vfork()								//创建子进程
    exec() family								//替代子进程
    atexit()/on_exit()/exit()/_exit()/_Exit()	//退出子进程
    wait()/waitpid()							//获得子进程退出状态
    

    getpid()、getppid()

    //getpid() 返回调用进程的PID
    //getppid() 返回调用进程的父进程的PID
    pid_t getpid(void); 		//pid_t是int
    pid_t getppid(void); 
    

    getuid()、geteuid()

    getuid()返回调用进程的UID
    geteuid()返回调用进程的effective UID
    uid_t getuid(void); 		//uid_t是unsigned int
    uid_t geteuid(void); 
    

    getgid(),getegid()

    //getgid()返回调用进程的real GID
    //getegid()返回调用进程的effective GID
    id_t getgid(void); 		//gid_t是unsigned int
    gid_t getegid(void); 		
    
    printf("pid=%d
    ",getpid());
    	printf("ppid=%d
    ",getppid());
    	printf("uid=%d
    ",getuid());
    	printf("gid=%d
    ",getgid()); 
    }
    

    fork()

    //创建子进程,在父进程中返回子进程的PID,在子进程中返回0,失败在父进程中返回-1
    pid_t fork(void); 
    

    fork()创建的子进程继承父进程的有:

    • 实际用户ID,实际组ID,有效用户ID,有效组ID
    • 附属组ID
    • 进程组ID
    • 会话ID
    • 控制终端
    • 设置用户ID标志和设置组ID标志
    • 当前工作目录
    • 根目录
    • 文件模式和安排
    • 信号屏蔽和安排
    • 对任一打开fd的close-on-exec
    • 环境
    • 连接的共享存储段
    • 存储映像
    • 资源限制

    与父进程有区别的有

    • fork的返回值
    • PID
    • PPID
    • 子进程的tms_utime,tms_stime,tms_cutime,tms_ustime被设置为0
    • 不继承文件锁
    • 子进程未处理闹钟被清除
    • 子进程未处理信号集设置为空集

    父子进程代码区执行次序

    fork()产生的所有进程共享代码区,copy-on-write其他区)

    • fork()之前的代码, 由parent执行一次
    • fork()之后的代码, 由父子进程各执行一次
    • fork()的返回值由父子进程各自返回一次

    copy-on-write:

    fork()一下干的几件事:

    • 给P2分配Text段, Data段, Heap段, Stack段的虚拟地址,都指向P1中相应的物理地址
    • P2的Text段是铁定和P1共享同一个物理地址了, 剩下的Data,Heap,Stack待定
    • 如果one of them 改变了这三个段的内容, 就把原来的数据复制一份给P2, 这样P2就有了相应的新的物理地址
    //创建任意多个进程:子进程干活,父进程创建一个爹一堆儿子
    int i=0;
    for(i=0;i<10;i++){	//创建10个进程, 只有parent在执行for()因为child在每次循环体内就exit()了
    	pid_t pid=fork();
    	if(-1==pid)
    		perror("fork"),exit(-1);
    	if(0==pid){
    		…
    		exit(0);	//终止子进程, 自然也就跳出了循环,防止再fork()
    	}
    }
    
    #include<unistd.h>
    #include<stdlib.h>
    pid_t pid=fork();
    if(-1==pid)
    	perror(“fork”),exit(-1);
    if(0==pid){
    	//child process
    	exit(0);
    }
    
    int main(){
    	pid_t pid=fork();
    	if(-1==pid)
    		perror("fork"),exit(-1);
    	printf("pid=%d
    ",pid);
    	if(0==pid){
    		printf("I'm child,my PID:%d,my parent's PID:%d
    ",getpid(),getppid());
    		sleep(3);
    		printf("I'm child,my PID:%d,my parent's PID:%d
    ",getpid(),getppid());
    	}else{
    		sleep(1);
    		printf("I'm parent,my PID:%d, my child's PID:%d
    ",getpid(),pid);
    	}
    	return 0;
    }
    $./a.out 
    pid=2915
    pid=0
    I'm child,	  my PID:2915, my parent's PID:2914
    I'm parent,my PID:2914, my child's   PID:2915
    I'm child,  my PID:2915, my parent's PID:1	#一个Orphan
    #卡在这, 因为两个进程抢一个终端,不是死循环,直接[ENTER]就行
    

    vfork()

    //创建一个空的子进程,父进程会等待子进程退出之后在继续执行,在子进程执行期间,父进程被挂起,此期间子进程和父进程共享所有的内存资源
    //vfork()多用在在不拷贝父进程页表的情况下创建新的进程,单独使用没有多线程的价值, 主要和exec()搭配使用。
    //子进程终止时不能从当前函数返回/调用exit函数, 可以调用_exit(), 该函数保证了子进程先于父进程执行
    //当下很多系统已经不再支持vfork()函数
    pid_t vfork(void);
    
    int main(){
    	pid_t pid=vfork();
    	if(-1==pid)
    		perror("vfork"),exit(-1);
    	if(0==pid){
    		printf("child %d starts
    ",getpid());
    		sleep(2);
    		//跳转出去, 调用execl()
    		int res=execl("./proc","proc",NULL);	
    		//"ls"表示执行方式, 以字符串的形式传进来
    		if(-1==res)
    			perror("execl"),_exit(-1);//ATTENTION,用_exit()
    	}
    	printf("parent starts
    ");
    	printf("parent ends
    ");
    	return 0;
    }
    //execl()可以跳出当前进程(VS fork()), 去执行一个完全不同的文件,可以帮助vfork()实现多进程,
    //父进程结束了,系统就会显示[~/Desktop/160512/Code]$,此时发现从已经终结的子进程跳转出的的文件还没执行完, 再打印ls -l的内容
    $./a.out 
    child 4258 starts
    parent starts
    parent ends
    [~/Desktop/160512/Code]$total 20
    -rw-rw-r-- 1 tarena tarena  754  5月 12 11:03 01waitpid.c
    -rw-rw-r-- 1 tarena tarena  449  5月 12 10:31 02vfork.c
    -rw-rw-r-- 1 tarena tarena  489  5月 12 11:28 03execl.c
    -rwxrwxr-x 1 tarena tarena 7499  5月 12 11:28 a.out
    */
    

    exec()

    用一个新的进程影像替代当前的进程映像,失败返回-1设errno
    extern char **environ;
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);
    int execle(const char *path, const char *arg, ..., char * const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[], char *const envp[]);
    

    ATTENTION: vfork()主要与exec family搭配使用, 主要用语子进程执行与父进程完全不同代码段的场合中, 其中vfork()专门用于创建进程, exec family 专门用于跳转执行
    , fork()虽然也可以和exec family 搭配使用, 但是fork()会复制父进程的内存空间, 复制完了又跳出去, 没什么意义, 效率不如(vfork(), exec family)

    #include<unistd.h>
    #include<sys/types.h>
    if(0==pid){
    		int res=execl("./proc","proc",NULL);	//"ls"就是执行方式, 以字符串的形式传进来, “proc”也是
    		if(-1==res)
    			perror("execl"),_exit(-1);		//ATTENTION,vfork用_exit()
    }
    

    7种进程终止

    • 正常终止:

      1. 从 main() 返回
      2. 调用 exit() / _exit() / _Exit()
      3. 最后一个线程从其启动例程返回
      4. 最后一个线程调用pthread_exit()
    • 异常终止:

      1. 调用abort()
      2. 接到一个信号并终止
      3. 最后一个线程对取消请求作出响应

    exit status VS termination status

    退出状态exit status是我们传入到exit(),_exit(),_Exit()函数的参数。进程正常终止的情况下,内核将退出状态转变为终止状态以供父进程使用wait(),waitpid()等函数获取。终止状态termination status除了上述正常终止进程的情况外,还包括异常终止的情况,如果进程异常终止,那么内核也会用一个指示其异常终止原因的终止状态来表示进程,当然,这种终止状态也可以由父进程的wait(),waitpid()进程捕获。

    exit()

    //引起进程的正常终止,所谓正常终止是按照注册的反顺序依次调用atexit()和on_exit()里注册的函数。VS _exit()和_Exit()会立即终止进程
    //进程终止后会传递退出码给父进程,这个"退出码&0377"可以被父进程的wait()系列函数捕获并解析。
    //系统使用8位二进制表示进程退出号,就是0~255,这也是为什么exit()返回status&0377给父进程的原因, 其实是取低八位二进制. 如果exit(10000),实际返回的就是16. 
    void exit(int status);
    

    atexit()

    //注册一个正常终止进程时执行的函数,这个函数的参数必须是void,注册成功返回0,失败返回非0
    int atexit(void (*function)(void));	//参数是函数指针
    

    on_exit()

    //和atexit()类似,用于注册exit()时执行的函数, 不同之处是on_exit注册的函数可以带参数,这个function的两个形参分别是通过exit()传入的int型 和 通过on_exit()传入的*arg
    //同一个函数可以被多次注册,注册一次退出进程时就会被执行一次
    //fork()出的子进程会继承父进程的注册函数,但一旦调用了exec(),所有注册的函数都会被移除
    //成功返回0,失败返回非0
    
    int on_exit(void (*function)(int , void *), void *arg);
    
    #include<stdlib.h>
    void fa(int status,void* pv){
    	printf("obtained status=%d
    ",status);
    	free(pv);
    	pv=NULL;
    }
    int main(){
    	pid_t pid=fork();
    	if(0==pid){
    		…
    		int *pi=(int*)malloc(sizeof(int));	//没有错误处理
    		if(0!=on_exit(fa,pi))
    			perror("on_exit"),exit(-1);
    		…
    		exit(100); //terminate child and free dynamic memory automatically
    	}
    }
    

    _exit()/_Exit():

    //立即终止调用的进程,所有的子进程都会挂到PID1下,父进程会收到SIGCHLD信号,还可以用wait()接收退出码
    
    void _exit(int status);		//<unistd.h>
    void _Exit(int status);		//<stdlib.h>
    
    #include<unistd.h>
    #include<sys/types.h>
    #include<stdio.h>
    #include<stdlib.h>
    void fa(){
    	printf("所有需要善后处理的工作都可以交给我哦!
    ");
    }
    int main(){
    	int res=atexit(fa);
    	if(0!=res)
    		perror("atexit"),exit(-1);
    	printf("main start
    ");
    	_exit(0);      	//立即终止,如果这句话留着fa()不会执行
    	printf("main end
    ");
    	return 0;		//把_exit()注释掉就会执行fa()
    }
    

    Orphan VS Zombie

    Orphan Process:一个parent退出,而它的一个或多个child还在运行,那么这些child将成为orphan。将被init(PID==1)收养,并由init对它们完成状态收集工作。init会循环地wait()直到这些child完成了他们的工作. 即当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

    Zombie Process: 一个使用fork()创建的child,如果child退出,而parent并没有调用wait/waitpid获取child的状态信息,那么child的process descriptor、PID和PCB等资源仍然保存在系统中。此时的child就变成了zombie。因为系统的PID总数是有限的, parent不断的创建child而不去wait,系统早晚会被拖垮.

    总结:

    • Orphan/Zombie都是因为在parent中没有wait掉child, 不同之处是orphan的parent已经没了, 由init来接管了,而zombie有个缺德的parent, 不wait还不撒手,拖累了系统
    • $ps 一下Zombie的进程状态是’Z’

    wait(), waitpid(), waitid()

    //wait for process to change state
    //wait()挂起父进程,直到一个子进程结束
    //waitpid()挂起父进程,直到指定的子进程终止
    //wait()相当于waitpid(-1, &status, 0)
    //成功返回子进程的PID,失败返回-1设errno
    
    pid_t wait(int *status);
    pid_t waitpid(pid_t pid, int *status, int options);
    
    /* pid in waitpid() and kill()
                    									
    pid>0	//指定pid
    pid=0	//GID是调用进程PID的子进程
    pid=-1	//任何子进程
    pid<-1	//GID是PID的子进程
    */
    /*options(Bitwaise Or) 
    WNOHANG     	//如果没有子进程终止就立即返回return immediately if no child has exited.
    WUNTRACED   	//如果一个子进程stoped且没有被traced,那么立即返回
    WCONTINUED (since Linux 2.6.10) //如果stoped的子进程通过SIGCONT复苏,那么立即返回 
    */
    /*
    如果退出不是NULL,wait()会使用形参指针带出退出码,这个退出码可以使用下列宏解读
    WIFEXITED(status)		//如果子进程正常退出返回真
    WEXITSTATUS(status)		//返回子进程的退出码,当且仅当WIFEXITED为真时有效
    WIFSIGNALED(status)		//如果子进程被一个信号终止时返回真
    WTERMSIG(status)		//返回终止子进程的信号编号,当且仅当WIFSIGNALED为真时有效
    WCOREDUMP(status)		//如果子进程导致了"核心已转储"则返回真,当且仅当WIFSIGNALED为真时有效r
    WIFSTOPPED(status)		//如果子进程被一个信号暂停时返回真,当且仅当调用进程使用WUNTRACED或子进程正在traced时有效
    WSTOPSIG(status)		//返回引起子进程暂停的信号编号,当且仅当WIFSTOPPED为真时有效
    WIFCONTINUED(status)(since Linux 2.6.10)//如果子进程收到SIGCONT而复苏时返回真
    */
    
    if(0==pid){
    	…
    	exit(100);		//把child的退出状态信息设为100
    }
    int status=0;
    int res=wait(&status);	//status用来接收结果
    if(-1==res)
    	perror("wait"),exit(-1);
    if(WIFEXITED(status))				//ATTENTION:这个宏要int不是int*,和wait不一样
    	printf("child%d end normally, status is:%d
    ",res,WEXITSTATUS(status));	//将打印出exit()里的状态
    

    例子

    /*------
    file.c
    ------*/
    #include<unistd.h>
    #include<fcntl.h>
    #include<sys/types.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    int main(){
    	int fd=open("a.txt",O_RDWR|O_TRUNC|O_CREAT,0664);
    	pid_t pid=fork();
    	if(-1==pid)
    		perror("fork"),exit(-1);
    	if(0==pid){
    		printf("child:%d start
    ",getpid());
    //		char buf[]="world";		//这个变量没必要,常量就行
    		int res=write(fd,"hello",sizeof("hello"));
    		if(-1==res)
    			perror("child write"),exit(-1);
    		res=close(fd);			//child把parent的数据复制过来,所以fd需要关闭
    		if(-1==res)
    			perror("child close"),exit(-1);
    		printf("parent:%d end
    ",getpid());
    		exit(0);
    	}
    	printf("parent:%d start
    ",getpid());
    	sleep(1);
    	int res=write(fd,"world",sizeof("world"));
    	if(-1==res)
    		perror("parent write"),exit(-1);
    	res=close(fd);
    	if(-1==res)
    		perror("parent close"),exit(-1);
    	printf("parent:%d end
    ",getpid());
    	return 0;
    }
    

    Note:

    • 运行结果a.txt两个进程没有覆盖=>父子进程使用的读写位置信息是同一份=>文件表是同一份=>但是两个是不同的fd, 所以fork()创建子进程也会复制一个文件描述符总表
    • 正是因为使用读写一次 offset会向后移, 所以没有覆盖, 因为后来的是使用前面留下的offset的位置, 所以使用的读写信息是一样的
    /*--------------------------------------------
    child终止时自动释放malloc()
    ----------------------------------------------*/
    #include<unistd.h>
    #include<sys/types.h>
    #include<stdio.h>
    #include<stdlib.h>
    #define PI 3.141592657
    int *ipdata=NULL;
    ///*
    void fa(){
    	free(ipdata);
    	ipdata=NULL;
    }
    //*/
    int main(){
    	pid_t pid=fork();
    	if(-1==pid)
    		perror("fork"),exit(-1);
    	if(0==pid){
    		printf("1");
    		printf("child starts
    ");
    		ipdata=(int*)malloc(sizeof(int));
    		if(NULL==ipdata)
    			printf("malloc fails
    ");
    //			perror("malloc"),exit(-1);	//???
    //		*ipdata=data;
    		int res=atexit(fa);
    		if(0!=res){
    			printf("atexit fails
    ");
    			exit(EXIT_FAILURE);
    		}
    
    		printf("Please input a radius:");
    		scanf("%d",ipdata);
    		printf("primeter is:%lf
    ",2*PI*(*ipdata));
    		exit(EXIT_SUCCESS);
    	}
    	printf("parent starts
    ");
    	int stat=0;
    	pid=wait(&stat);
    	if(WIFEXITED(stat))
    		printf("child has terminated,its status:%d
    ",WEXITSTATUS(stat));
    	return 0;
    }
    

    Note:

    • 用全局变量做桥梁
    • atexit()里面的函数一定是int *(void)函数的形参列表变了也不行
    • ATTENTION: vfork()的child虽然整个内存区都是和parent共享的, 但是变量的作用域还是在啊, 所以你跨函数使用变量肯定要传参的啊
    /*--------------------------------------------
    on_exit.c, child终止时自动释放malloc()
    ----------------------------------------------*/
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<sys/wait.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<signal.h>
    void fa(int status,void* pv){
    	printf("obtained status=%d
    ",status);
    	free(pv);
    	pv=NULL;
    }
    
    int main(){
    	//创建子进程,使用fork()
    	pid_t pid=fork();
    	if(-1==pid){
    		printf("parent starts%d
    ",pid);
    	}
    	if(0==pid){
    		printf("child starts%d
    ",getpid());
    		int *pi=(int*)malloc(sizeof(int));
    		if(NULL==pi)
    			printf("malloc error"),exit(-1);
    		//use on_exit()register function
    		if(0!=on_exit(fa,pi))
    			perror("on_exit"),exit(-1);
    		printf("please input a radius(int)
    ");
    		scanf("%d",pi);
    		printf("the primeter is:%lf
    ",2*3.14*(*pi));
    		//terminate child and free dynamic memory automatically
    		exit(100);
    	}
    
    	//父进程等待子进程终止, 获取退出状态
    	int status=0;
    	int res=waitpid(pid,&status,0);
    	if(-1==res)
    		perror("waitpid"),exit(-1);
    	if(WIFEXITED(status))
    		printf("status of child:%d
    ",WEXITSTATUS(status));
    	return 0;
    }
    
  • 相关阅读:
    根据某字符(字符串)分割字符串
    call函数心得
    Git之常用命令
    ES6之async与await
    CSS之 sass、less、stylus 预处理器的使用方式
    JavaScript之继承
    vue之keep-alive的使用
    CSS之单行、多行文本溢出显示省略号
    Vue之 watch、computed、filter之间的区别与使用场景
    Vue之watch监听对象中某个属性的方法
  • 原文地址:https://www.cnblogs.com/xiaojiang1025/p/5934317.html
Copyright © 2011-2022 走看看