1.创建进程
- fork函数创建进程
#include
/*
功能:创建子进程
参数:无
返回值:失败返回-1
*/
pid_t fork(void);
注意:
1.创建成功时候,父进程返回子进程的进程号,子进程返回0
2.通过fork返回值区分父进程和子进程
举例:
#include
#include
int main(void)
{
int count = 0;
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork");
return -1;
}
else if (pid == 0) {
printf("child process : my pid is %d\n", getpid());
count++;
printf("%d\n", count);
}
else {
printf("parent process : my pid is %d\n", getpid());
count++;
printf("%d\n", count);
}
return 0;
}
结果如下:
hanqi@hanqi-PC:~/C/process$ ./a.out
parent process : my pid is 8885
1
child process : my pid is 8886
1
1.此处在pid=fork()之前,只有1个进程在执行这段代码,但在这条语句之后,就变成2个进程在执行了,这两个进程的几乎完全相同.此处比较神奇的是,从执行结果来看fork()执行了1次却返回了2个值,一个等于0,一个大于0. 其中等于0的是子进程,大于0的是父进程.
2.创建新进程成功后,在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID.此时系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
3.执行完fork后,进程1的变量为count=0,进程2的变量为count=0,这两个进程的变量都是独立的存在不同的地址中,不是共用的,这点要注意.可以说,我们就是通过fpid来识别和操作父子进程的。
- vfork创建进程
1.vfork直接使用父进程的存储空间,不拷贝
2.vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行
例如:
#include
#include
#include
#include
int main(void)
{
pid_t pid;
pid = vfork();
if(pid > 0)
{
while(1)
{
printf("father process: %d\n", getpid());
sleep(1);
}
}
else if(pid == 0)
{
while(1)
{
printf("son process: %d\n", getpid());
sleep(1);
//exit(0);
}
}
return 0;
}
子进程没有exit(0)的时候,程序会一直打印子进程,不会执行父进程代码.子进程有exit(0)的时候先执行子进程代码,再执行父进程代码
2.父子进程关系
- 子进程继承了父进程内容,包含父进程代码,变量,堆栈,文件等
- 父子进程有独立的地址空间,互不影响
- 若父进程先结束,则其子进程成为孤儿进程,被init进程(pid=1)收养;且子进程变成后台进程
- 若子进程先结束,如果父进程没有及时回收,子进程会变成僵尸进程
3.其他思考
- 子进程从何处开始运行?
答:子进程从fork()的下一条语句开始执行.因为在父进程中执行fork()时候,其PC指针寄存器指向fork()下一条语句,此时子进程被创建.子进程会复制父进程内容,包括PC指针寄存器,所以此时子进程PC寄存器与父进程PC寄存器相同,故执行fork()下一条指令. - 父子进程执行顺序?
答:按照内核调度执行 ,谁都可以先执行 - 父进程能否多次调用fork?子进程呢?
答:父进程可以多次调用fork,每调用一次fork就会创建一个新的子进程.子进程亦可,调用后产生孙进程
4.结束进程
- exit/_exit函数结束进程
/*
功能:结束进程
参数:返回给父进程的状态值
返回值:无
*/
#include
#include
void exit(int status);
void _exit(int status);
注意:
1.exit头文件是stdlib.h,_exit是unistd.h
2.exit结束进程时候会刷新缓冲区,_exit()不会
举例:
#include
#include
int main(void)
{
printf("hello");
exit(0);
printf("world");
}
结果如下:
hanqi@hanqi-PC:~/C/process$ ./a.out
hellohanqi@hanqi-PC:~/C/process$ gcc test.c
分析:打印hello时候没有加入换行符,所以数据不会立刻打印到终端上,而是存放在缓冲区内,当执行exit时候,会刷新缓冲区,打印hello,之后退出进程,故没打印world.
#include
#include
int main(void)
{
printf("hello\n");
printf("world");
exit(0);
}
结果如下:
hanqi@hanqi-PC:~/C/process$ ./a.out
hello
worldhanqi@hanqi-PC:~/C/process$
#include
#include
int main(void)
{
printf("hello\n");
printf("world");
_exit(0);
}
执行结果如下:
hanqi@hanqi-PC:~/C/process$ ./a.out
hello
hanqi@hanqi-PC:~/C/process$
分析:可以看到,并未打印world,因为word被存入缓冲区,而_exit不会刷新缓冲区,所以不会打印
5.exec函数族
- 进程调用exec()函数族执行某个程序
- 此时进程当前内容被指定的程序替换,子进程PID不变
- 最终实现让父子进程执行不同的程序
- 优点:可调用系统指令,使得代码更简洁
- 实现步骤:
1.父进程创建子进程
2.子进程调用exec函数族
3.父进程不受影响
如shell程序 - execl/execlp函数
#include
/*
功能:在子进程中执行指定程序
参数:
参数1:指定程序路径及名称
参数2:传递给执行程序的参数列表
返回值:失败返回-1,成功不返回
*/
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
注意:
1.execl函数,且最后一个参数为空指针NULL
2.execlp函数第一个参数直接传入程序名称,函数自动在PATH中查找路径
举例:执行ls命令,显示/etc目录下所有文件的详细信息
if (execl("/bin/ls", "ls", "-a", "-l", "/etc", NULL) < 0) {
perror("execl");
}
if (execlp("ls", "ls", "-a", "-l", "/etc", NULL) < 0) {
perror("execl");
}
- execv/execvp函数
#include
/*
功能:在子进程中执行指定程序
参数:
参数1:指定程序路径及名称
参数2:传递给执行程序的参数数组
返回值:失败返回-1,成功不返回
*/
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
举例:执行ls命令,显示/etc目录下所有文件的详细信息
char *arg[] = {"ls", "-a", "-l", "/etc", NULL};
if (execv("/bin/ls", arg) < 0) {
perror("execl");
}
if (execlvp("ls", arg) < 0) {
perror("execl");
}
6.system
- 创建子进程并执行command命令,即shell指令
- 其内部就是调用了exec函数族
- 子进程结束后会回到父进程执行
#include
/*
功能:创建子进程并执行command命令
参数:执行命令
返回值:失败返回-1,成功返回命令command的返回值
*/
int system(const char *command);
注意:
父进程会等待子进程command执行结束后才继续执行
6.5popen
- ponen功能与system类似,都是执行特定指令
- 但是popen会返回运行结果
#include
/*
功能:创建子进程并执行command命令,返回执行结果
参数:
参数1:要执行的命令
参数2:类型,读(r)或写(w)
返回值:失败返回-1,成功返回一个流指针,并记录结果指令执行结果的字节数
*/
FILE *popen(const char *command, const char *type);
- 举例:
#include
#include
#include
int main()
{
char res[1024] = {0};
FILE *fp;
fp = popen("ps", "r");
int read_byte = fread(res, sizeof(char), 1024, fp);
printf("read %d byte, res is \n%s", read_byte, res);
return 0;
}
此处popen函数会调用ps指令查看当前进程信息,并将查询结果给res数组保存,组后打印
- 结果如下:
read 146 byte, res is
PID TTY TIME CMD
4783 pts/1 00:00:00 bash
7117 pts/1 00:00:00 a.out
7118 pts/1 00:00:00 sh
7119 pts/1 00:00:00 ps
7.进程回收
- 在linux系统下子进程结束时候由父进程回收
- 孤儿进程统一由init进程回收
- 子进程先结束,如果父进程没有及时回收,子进程会变成僵尸进程
- 函数wait
#include
/*
功能:回收子进程
参数:指向保存子进程返回值和结束方式的地址或者NULL,表示父进程不接受返回值
返回值:失败返回-1,成功返回被回收的子进程的进程号
*/
pid_t wait(int *status);
注意:
1.wait函数由父进程调用
2.若子进程没有结束,则父进程会一直处于阻塞状态
3.若父进程有多个子进程,哪个子进程先结束就先回收哪个子进程
进程回收举例:
int status;
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork");
exit(-1);
}
else if (pid == 0) {
sleep(1);
exit(2);//子进程执行
}
else {
wait(&status);
printf("%x\n", status;)
}
- 子进程正常结束方式
1.子进程通过exit/_exit/return返回某个值(0~255),即返回数据的低8位,如果超过8位,则高位会被舍弃
2.父进程调用wait(&status)回收
WIFEXITED(status) //判断子进程是否正常结束,非正常返回0
WEXITSTATUS(status) //获取子进程返回值
WIFSIGNALED(status) //判断子进程是否被信号结束
WTERMSIG(status) //获取结束子进程的信号类型
status为int类型,一共32位,但只有低16为有效
bit0~bit6代表子进程结束方式,如果为0表示子进程正常结束,非0表示子进程被信号结束且值为结束信号类型
bit8~bit15代表子进程正常结束时候的返回值
- 举例:
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
int status = 10;
pid = fork();
if(pid > 0)
{
wait(&status);
printf("child process exit,status=%d\n", WEXITSTATUS(status));
while(1)
{
printf("father process: %d\n", getpid());
sleep(1);
}
}
else if(pid == 0)
{
while(1)
{
printf("son process: %d\n", getpid());
sleep(1);
exit(3);
}
}
return 0;
}
使用WEXITSTATUS宏来获取子进程返回值,此处为3,而且父进程阻塞在wait函数处,直到子进程结束
- 函数waitpid
#include
/*
功能:回收子进程
参数:
参数1:指定回收的对象,即子进程进程号
参数2:指向保存子进程返回值和结束方式的地址或者NULL,表示父进程不接受返回值
参数3:指定回收方式参数为:0(阻塞方式)或者WNOHANG(非阻塞)
返回值:失败返回-1,成功返回被回收的子进程的进程号或者0(此时表示子进程未结束)
*/
pid_t waitpid(pid_t pid, int *status, int option);
- 举例:
waitpid(pid, &status, 0);//以阻塞方式回收指定子进程
waitpid(pid, &status, WNOHANG);//以非阻塞方式回收指定子进程
waitpid(-1, &status, 0);//以阻塞方式回收任意子进程
waitpid(-1, &status, WNOHANG);//以非阻塞方式回收任意子进程
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
int status = 10;
pid = fork();
if(pid > 0)
{
waitpid(pid, &status, WNOHANG);
printf("child process exit,status=%d\n", status);
while(1)
{
printf("father process: %d\n", getpid());
sleep(1);
}
}
else if(pid == 0)
{
int cnt = 0;
while(1)
{
printf("son process: %d\n", getpid());
sleep(1);
cnt++;
if(cnt >= 4)
{
exit(3);
}
}
}
return 0;
}
注意:使用非阻塞方法仍然会产生僵尸进程
9.获取进程号
- 函数getpid
#include
#include
/*
功能:获取进程号
参数:无
返回值:当前程序进程号
*/
pid_t getpid(void);
- 举例,获取当前程序进程号
#include
#include
#include
int main()
{
pid_t pid;
pid = getpid();
printf("pid = %d\n", pid);
while(1);
return 0;
}
8.守护进程
- 守护进程概念:
1.守护进程(Daemon)是Linux三种进程类型之一
2.守护进程通常在系统启动时运行,系统关闭时结束
3.守护进程在Linux系统中大量使用,很多服务程序以守护进程形式运行 - 守护进程特点:
1.始终在后台运行
2.独立于任何终端
3.周期性的执行某种任务或等待处理特定事件 - 会话,控制终端概念
1.Linux通过会话(session),进程组的方式管理进程
2.每个进程属于一个进程组,即每运行一个程序就创建了一个进程组
3.会话是一个或多个进程组的集合.通常用户打开一个终端时,系统会创建一个会话.所有通过该终端运行的进程都属于这个会话,其中首进程为shell进程
4.终端关闭时,所有相关的进程都会被结束 - 守护进程创建步骤:
1.创建子进程,父进程退出,此时子进程会变为孤儿进程,被init进程收养,子进程会在后台运行,但此时子进程仍然依附于终端
if(fork() > 0){
exit(0);
}
2.子进程创建新会话,此时子进程会成为新的会话组组长,子进程脱离原终端会话
if(setsid() > 0){
exit(0);
}
3.更改当前工作目录,守护进程一直在后台运行,其工作目录不能被卸载
chdir("/");
chdir("/temp"); //权限为0777
4.重新设定文件权限掩码为0,掩码只对当前进程有效
if(umask(0) < 0){
exit(-1);
}
5.关闭打开的文件描述符,关闭从父进程继承的文件,即关闭父进程打开的文件.因为守护进程独立于终端,而其本身继承了父进程的标准输入输出错误流,这些文件依赖于原来父进程的会话,所以在子进程中无法使用,需要关闭
int i;
for(i = 0; i < getdtablesize(); i++)
{
close(i);
}
举例:创建守护进程,每个1s将系统时间写入文件time.log
#include
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
pid_t pid;
FILE* fout;
time_t t;
//创建子进程
if ((pid = fork()) < 0) {
perror("fork");
exit(-1);
}
//关闭父进程,让子进程变为孤儿进程
else if (pid > 0) {
exit(0);
}
setsid();//子进程创建新会话
umask(0);//修改文件权限掩码
chdir("/tmp");//修改工作目录
for (int i = 0; i < getdtablesize(); i++) {
close(i);//关闭父进程文件描述符
}
//创建输出流
if ((fout = fopen("time.log", "a")) == NULL) {
perror("fopen");
exit(-1);
}
while (1)
{
time(&t);
fprintf(fout, "%s", ctime(&t));
fflush(fout);
sleep(1);
}
}
执行结果:在/tmp文件夹下每过1秒会在后台打印系统时间给time.log