进程概念
ps -elf:查看操作系统的所有进程(Linux命令)
ctrl + z:把进程切换到后台
crtl + c:结束进程
fg:把进程切换到前台
获取进程进程号和父进程号
函数原型:
pid_t getpid(void); //pid_t,它是一个有符号整数类型。
pid_t getppid(void);
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{pid_t pid = getpid();printf("当前进程的进程号为:%d\n", pid);pid_t ppid = getppid();printf("当前进程的父进程为:%d\n", ppid);while(1);return 0;
}
fork
概念:fork() 是一个在操作系统编程中常用的函数,用于创建一个新的进程。它通过复制调用进程(称为父进程)来创建一个新的进程(称为子进程)。子进程是父进程的副本,它从 fork() 函数返回的地方开始执行。
以下是 fork() 函数的原型:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork() 函数没有参数,它返回一个 pid_t 类型的值,表示进程的状态。返回值有以下几种情况:
- 如果返回值是负数(-1),则表示创建子进程失败。
- 如果返回值是零(0),则表示当前代码正在子进程中执行。
- 如果返回值是正数,则表示当前代码正在父进程中执行,返回值是新创建子进程的PID。
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int main()
{ pid_t pid = fork();if(pid == -1){perror("fork");exit(1);}else if(pid == 0){ printf("child pid=%d, getpid=%d, getppid=%d\n", pid, getpid(), getppid());
// while(1)
// {printf("child\n");sleep(1);
// }}else { printf("parent pid=%d, getpid=%d, getppid=%d\n", pid, getpid(), getppid());
// while(1)
// {printf("parent\n");sleep(2);
// }}printf("helloworld\n");//会输出两次return 0;
}
fork笔试题
详情看下述代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{for(int i = 0; i < 2; i++){ fork();// printf("-\n"); //6个"-",换行符会输出缓冲区里的的数据printf("-"); // 8个"-",子进程会复制父进程输出缓冲区的数据} return 0;
}
fork原理
下面输出都为1的原因是,父子进程在不同的空间
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{ int num = 0;if(fork() == 0){ num++;printf("child %d\n", num);} else{ num++;printf("parent %d\n", num);}/*输出为:child 1parent 1*/return 0;}
多进程读写
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>void child_write(int fd)
{char buf[128] = {0};while(1){scanf("%s", buf);if(write(fd, buf, strlen(buf)) == -1){perror("write");break;}lseek(fd, -1 * strlen(buf), SEEK_CUR);if(!strcmp(buf, "bye"))break;memset(buf, 0, 128);}//i lseek(fd, -1 * strlen(buf), _CUR);}void parent_read(int fd)
{char buf[128] = {0};while(1){int ret = read(fd, buf, sizeof(buf));if(ret == -1){perror("read");break;}else if(ret == 0)continue;if(!strcmp(buf, "bye"))break;printf("child get: %s\n", buf);memset(buf, 0, sizeof(buf));}
}int main()
{int fd = open("hello.txt", O_CREAT | O_RDWR, 00400 | 00200);if(-1 == fd){perror("open");exit(1);}if(fork() == 0){child_write(fd);}else{parent_read(fd);}close(fd);return 0;
}
vfork
vfork 是一个在某些操作系统中提供的系统调用函数,用于创建一个新的进程,并与父进程共享内存空间。与 fork 不同的是,vfork 在创建子进程时不会复制父进程的内存空间,而是与父进程共享同一份内存。这使得 vfork 函数比 fork 函数更高效,因为它不需要复制整个父进程的内存空间。
vfork 函数的语法如下:
#include <unistd.h>
pid_t vfork(void);
返回值:vfork 函数没有参数,返回一个进程ID(PID)。在父进程中,vfork 返回子进程的PID;在子进程中,vfork 返回0。如果 vfork 调用失败,返回-1。
注意事项:
- 子进程的执行:在调用 vfork 后,子进程会暂停父进程的执行,直到子进程调用 exec 函数族中的一个函数或者调用 _exit 函数来终止自己。子进程在执行期间与父进程共享同一份内存空间,因此需要谨慎处理共享资源的访问,以避免出现竞争条件和数据损坏等问题。
- 父进程的阻塞:在调用 vfork 后,父进程会阻塞,直到子进程调用 exec 函数族中的一个函数或者调用 _exit 函数,或者导致异常终止。
- 返回值的使用:根据 vfork 的返回值可以判断当前代码是在父进程还是子进程中执行。在父进程中,返回的是子进程的PID;在子进程中,返回的是0。可以根据这个返回值来区分父子进程的执行路径。
- vfork创建的子进程需要指定退出方式
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int main()
{pid_t pid = vfork();if(pid == -1){perror("vfork");exit(1);}else if(pid == 0) //子进程{printf("pid = %d, getpid = %d, getppid = %d\n", pid, getpid(), getppid());sleep(2);exit(0);}else //父进程{printf("pid = %d, getpid = %d, getppid = %d\n", pid, getpid(), getppid());}return 0;
}
exec系列函数
execl
execl 是一个系统调用函数,用于在当前进程中执行一个新的程序。它会取代当前进程的代码和数据,加载并执行指定的程序文件。
execl 函数的原型如下:
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., (char *) NULL);
参数说明:
path:要执行的程序文件的路径。
arg0:新程序的第一个参数,通常是程序的名称。后续参数是新程序的命令行参数,以 NULL 结尾。
注意事项:当调用 execl 函数成功时,当前进程的代码和数据将被替换,之后的代码将不再执行。因此,如果在 execl 调用之后还有需要执行的代码,应该将其放在 execl 调用之前。
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{if(vfork() == 0){ printf("child: pid=%d\n", getpid());//新的进程一旦启动,父进程就开始执行execl("/usr/bin/cp", "cp", "-r", "/usr/local", ".", NULL);printf("hello world!");//不会输出,因为执行了execl就不再执行下面的代码了} else{printf("parent: pid=%d\n", getpid());} return 0;
}
拓展:
另外,execl 函数还有一些变种,如 execlp、execle、execv 等,它们在参数传递和执行方式上有所不同。可以根据具体的需求选择合适的函数来执行新程序。
孤儿进程 & 僵尸进程
孤儿进程:孤儿进程(Orphan Process)是指在父进程结束或被终止后,其子进程仍然在运行但失去了父进程的监管和控制。
孤儿进程的状态和行为有以下特点:
- 孤儿进程的父进程 ID(PPID)被设置为 init 进程的进程 ID(通常是 1)。
- 孤儿进程继续在系统中运行,但其父进程已经不存在。
- 孤儿进程的资源(如打开的文件描述符、内存等)不会被释放,因为它没有被正常地回收。
- 孤儿进程的终止状态(退出状态码)将被保存,直到父进程通过调用 wait 或 waitpid 等系统调用来获取。
-
孤儿进程的存在是为了避免子进程在父进程终止后变成僵尸进程(Zombie Process)。当父进程没有及时处理子进程的终止状态时,子进程将变成僵尸进程,占用系统资源。而孤儿进程的终止状态会被保存,直到被新的父进程处理。
-
在编写程序时,可以通过一些方式避免产生孤儿进程,例如在父进程终止之前等待子进程的终止,或者使用适当的进程管理和通信机制来确保子进程的正确终止和资源回收。
僵尸进程:僵尸进程(Zombie Process)是指一个已经终止执行的子进程,但其父进程尚未对其进行完全的资源回收和终止状态获取的进程。
僵尸进程的状态和行为有以下特点:
- 僵尸进程的状态(进程状态码)为 “Z” 或 “Z+”,在进程列表中以 “” 或 “Z” 标识。
- 僵尸进程的父进程仍然存在,但尚未调用相应的系统调用(如 wait 或 waitpid)来获取子进程的终止状态。
- 僵尸进程的资源(如打开的文件描述符、内存等)几乎没有消耗,因为它已经停止执行。
- 僵尸进程的终止状态(退出状态码)仍然保存在系统中,等待父进程来获取。
- 僵尸进程的存在是因为在 Linux 系统中,子进程的终止状态需要被父进程显式地获取。父进程可以通过调用 wait、waitpid 或 waitid 等系统调用来获取子进程的终止状态,并进行相应的资源回收。如果父进程没有及时处理子进程的终止状态,子进程就会变成僵尸进程。
- 僵尸进程一般不会对系统的正常运行产生直接影响,但如果大量的僵尸进程积累,可能会占用系统的进程表资源。因此,及时处理僵尸进程是良好的编程实践。
在编写程序时,可以通过以下方式避免僵尸进程的产生:
- 在父进程中使用 wait、waitpid 或 waitid 等系统调用来获取子进程的终止状态。
- 使用信号处理机制,在父进程中捕获子进程的终止信号(如 SIGCHLD),并在信号处理函数中处理子进程的终止状态。
- 使用进程间通信机制(如管道、信号量、共享内存等)来实现父进程与子进程之间的同步和通信,确保子进程的正确终止和资源回收。
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>int main()
{if(fork() == 0){ sleep(1);printf("child: pid = %d, ppid = %d\n", getpid(), getppid());exit(100);} else{ printf("parent: pid = %d\n", getpid());int status;wait(&status);if(WIFEXITED(status)) //判断子进程是否正常结束{ printf("子进程正常结束\n");printf("子进程退出状态:%d\n", WEXITSTATUS(status));} else{printf("子进程异常退出\n");}}return 0;
}