一、linux系统编程学习大纲
1、进程的概念、进程的诞生与死亡、进程基本函数接口,进程意义。
2、进程之间的通信方式:无名管道、有名管道、信号、消息队列、共享内存(信号量)
3、linux下信号集概念、信号集的函数接口、信号集作用、如何给信号集设置阻塞状态?
4、线程的概念、线程与进程有什么区别?线程诞生与死亡、线程一系列函数接口。
5、线程同步互斥方式:有名信号量、无名信号量、互斥锁、读写锁。
6、处理空闲线程的方式:条件变量。
7、线程池 -> 为了能够同时处理多个任务。
二、进程的概念
1、什么是程序?什么是进程?
程序就是一堆待执行的代码。 --> 静态的文本数据。(代码) --> 存放在硬盘中,客观存在
例如:
project.c -> C语言"程序"
project -> 可执行"程序"
特点:关机重启之后,文件依然还在。
进程就是当程序被CPU加载时,根据程序中每一行代码做出相应的效果,形成一个动态的过程,那么这个过程就是进程。 --> 动态的过程。(相当于正在热播的电视剧) , 存放在内存中
例如:
./project --> 形成了一个动态的过程(进程)
关机重启之后,进程不在。
2、在linux下,如何开启一个进程?
直接在终端上执行一个程序,就可以开启一个进程。
gec@ubuntu:~$ ./bin/project --> 就会开启一个名字叫./bin/project的进程。
3、当进程开启后,系统为进程分配什么资源?
1)会分配进程对应的内存空间。
-------------project.c-----------
int main()
{
int x;
return 0;
}
---------------------------------
当这个程序编译之后,运行时,系统就会在运行内存中分配4个字节给它。
2)当进程开启之后,系统会为进程分配一个任务结构体,这个任务结构体就是专门用于描述这个进程的。
struct task_struct{}
路径:/usr/src/linux-headers-4.10.0-38/include/linux/sched.h
三、关于查看进程信息的几个基本命令
1、查看整个系统中所有进程的ID号。 --> ps -ef(数据是静态)
PID PPID
gec 6781 2769 0 10:00 pts/20 00:00:00 bash
root 1 0 0 Jan11 ? 00:00:02 /sbin/init auto noprompt --> 祖先进程
PID:自身的ID号。 bash进程自身的ID号是6781
PPID:父进程的ID号。bash进程父进程ID号是2769
2、查看进程CPU使用率。 --> top(数据是动态,每隔3s就会更新一次)
gec@ubuntu:~$ top -> 按'q'返回终端。
Tasks: 240 total, 1 running, 174 sleeping, 0 stopped, 0 zombie
运行态 睡眠态 暂停态 僵尸态
%CPU
1189 root 20 0 485500 58380 19816 S 3.7 5.9 0:35.88 Xorg
2769 gec 20 0 637344 40860 25880 S 3.3 4.1 0:09.01 gnome-terminal-
3、查看整个系统所有进程之间的关系? --> pstree
PID PPID
root 1 0 0 Jan11 ? 00:00:02 /sbin/init auto noprompt
root 1172 1 0 Jan11 ? 00:00:00 /usr/sbin/lightdm
root 1478 1172 0 Jan11 ? 00:00:00 lightdm --session-child 12 19
gec 2118 1478 0 Jan11 ? 00:00:00 /sbin/upstart --user
gec 2769 2118 0 Jan11 ? 00:00:10 /usr/lib/gnome-terminal/gnome-terminal-server ---> 终端
gec 2774 2769 0 Jan11 pts/18 00:00:00 bash ---> 命令行
gec 6915 2774 0 10:35 pts/18 00:00:00 ps -ef ---> linux命令
gec@ubuntu:~$ pstree
systemd─lightdm─lightdm──upstart─gnome-terminal──bash──pstree
四、进程诞生与死亡。
进程的生老病死.jpg
1、什么是僵尸态?
进程结束时候,就会从运行态变成僵尸态,僵尸态其实就是代表这个进程占用的资源没有释放掉,那么这个状态就称之为僵尸态。
2、总结几个易错点?
1)进程在暂停态,如果收到继续的信号,那么就会切换到就绪态,而不是运行态。
2)程序中main函数执行了return 0就代表一个进程的结束,一定会变成僵尸态。
3)进程不可以没有父进程,也不能同时拥有两个父进程。
4)孤儿进程特征:失去父亲时,会马上寻找继父,而不是等到孤儿进程变成僵尸态之后再找。
5)祖先进程一定会帮其他进程回收资源。
五、进程意义?
目前而言,我们写的代码都是一行一行代码地去执行,这种程序称之为单进程程序。
但是学习进程目的是为了学习多进程编程,可以实现同一个时刻处理两件/多件事情。
六、进程的函数接口?
单进程程序 --> 只能一行一行代码地去执行。
多进程程序 --> 同时执行两行代码 --> 让本来的进程产生一个新的子进程,让子进程去处理另外一件事情。
1、如何在正在运行的进程中创建一个子进程? --> fork() --> man 2 fork
功能: fork - create a child process
//创建一个子进程
使用格式:
#include <unistd.h>
pid_t fork(void);
参数:无
返回值:pid_t就是进程ID号的数据类型
成功:会产生一个子进程
父进程 --> 返回子进程的ID号(x>0)
子进程 --> 0
失败:不会产生子进程
父进程 --> -1
例题1: 在进程内部创建一个子进程,看看会不会同时做两件事情。
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
//1. 目前而言,依然是一个单进程程序
printf("helloworld!\n");
printf("appletree!\n");
//2. 在当前的进程情况下,创建一个子进程
fork();
//3. 目前而言,就已经有两个进程了,一个是父进程,一个是子进程
printf("guangzhou!\n");
return 0;
}
运行结果1:(父进程先运行)
gec@ubuntu:/mnt/hgfs/code$ ./demo1
helloworld!
appletree!
guangzhou! --> 父进程打印
gec@ubuntu:/mnt/hgfs/code$ guangzhou! --> 子进程打印
运行结果2:(子进程先运行)
gec@ubuntu:/mnt/hgfs/code$ ./demo1
helloworld!
appletree!
guangzhou! --> 子进程打印
guangzhou! --> 父进程打印
gec@ubuntu:/mnt/hgfs/code$
结论:
1)父子进程执行顺序是随机的。
2)fork之后的代码,两个进程都会执行。
例题2: 想确保子进程先运行,做法:就是让父进程先睡眠,子进程不用睡眠。
意味着父子进程工作内容不一样。
大概分析结果:
pid_t x = fork();
if(x > 0) //父子进程都会判断,但是父进程能进去,子进程不能进去
{
//父进程工作内容
}
if(x == 0) //父子进程都会判断,但是父进程不能进去,子进程能进去
{
//子进程工作内容
}
-----------------------------------------------------
代码实现:
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
//1. 目前而言,依然是一个单进程程序
printf("helloworld!\n");
printf("appletree!\n");
//2. 在当前的进程情况下,创建一个子进程
pid_t x = fork();
//3. 目前而言,就已经有两个进程了,一个是父进程,一个是子进程
if(x > 0) //父进程才能进去
{
usleep(5000);
printf("parent print hello!\n");
}
if(x == 0) //子进程才能进去
{
printf("child print hello!\n");
}
return 0;
}
-----------------------------------------------------
练习1: 写一个程序,使得子进程每隔1s就打印一次apple(一共10次),父进程每隔2s就打印一次hello(一共5次)。
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t x;
int i;
x = fork();
if(x > 0) //父进程
{
for(i=0;i<5;i++)
{
printf("parent hello!\n");
sleep(2);
}
}
if(x == 0) //子进程
{
for(i=0;i<10;i++)
{
printf("child apple!\n");
sleep(1);
}
}
return 0;
}
2、在一个进程中查看自己的PID号以及父进程的PID号。
getpid() getppid() --> man 2 getpid
功能: getpid, getppid - get process identification
//获取进程的ID号
使用格式:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
参数:无
返回值:
成功:
getpid()返回自己的PID号。
getppid()返回父进程的PID号。
失败: 不可能失败!
练习2: 写一个程序,在子进程中输出自己和父进程的ID号,在父进程中输出自己和子进程的ID号。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
if(x > 0) //父进程
{
sleep(1);
//输出自己的PID号
printf("parent pid = %d\n",getpid());
//输出子进程的PID号
printf("child pid = %d\n",x);
}
if(x == 0) //子进程
{
//输出自己的PID号
printf("child pid = %d\n",getpid());
//输出父进程的PID号
printf("parent pid = %d\n",getppid());
}
return 0;
}
练习3: 验证孤儿进程是失去父亲时马上寻找继父,而不是等到自己变成僵尸再找。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
if(x > 0)
{
sleep(3);
printf("parent exit!\n");
}
if(x == 0)
{
sleep(1);
printf("my parent = %d\n",getppid()); //旧
sleep(2);
usleep(500000);
printf("my parent = %d\n",getppid()); //新
sleep(2);
printf("my parent = %d\n",getppid()); //新
}
return 0;
}
七、如果父进程还存在的情况下,如何解决僵尸问题?
1、父进程还在,还主动回收资源。 --> wait() --> man 2 wait
功能: wait for process to change state
//等待一个进程,并且改变这个进程状态 (把进程从僵尸态变成死亡态)
使用格式:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
参数:
status:监听子进程的退出值
填NULL 例如: wait(NULL) --> 代表父进程主动回收资源,但是不关心子进程的退出状态。
不填NULL 例如: int state;
wait(&state);--> 代表父进程主动回收资源,而且还很关心你的退出状态。
wait():属于一个阻塞函数
如果子进程还在运行态,那么这个函数就会阻塞,一直阻塞等待到子进程变成僵尸为止。
如果子进程已经是僵尸态,那么这个函数就会直接返回。
返回值:
成功:回收资源的那个子进程的ID号
失败:-1
例题2: 验证一下wait函数是可以帮子进程回收资源的。
情况一:父进程时间长,子进程时间短。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
if(x > 0)
{
printf("parent sleep...\n");
sleep(8);
wait(NULL);
printf("parent after wait\n");
}
if(x == 0)
{
printf("child sleep...\n");
sleep(3);
printf("child exit!\n");
}
return 0;
}
情况二:父进程时间短,子进程时间长。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
if(x > 0)
{
printf("parent sleep...\n");
sleep(3);
wait(NULL);
printf("parent after wait\n");
}
if(x == 0)
{
printf("child sleep...\n");
sleep(8);
printf("child exit!\n");
}
return 0;
}
2、父进程还在,但是不主动回收资源。
只能等到父进程结束之后,再寻找继父帮自己回收资源。
练习4: 验证以上的这种情况。
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
if(x > 0)
{
sleep(10);
printf("parent exit!\n");
}
if(x == 0)
{
sleep(3);
printf("child exit!\n"); //0~3 无僵尸 3~10 有僵尸 10之后 无僵尸
}
return 0;
}
八、进程的退出。 --> exit() / _exit() / _Exit()
这三个函数功能都是可以让进程退出,并且可以表示自己的退出状态,并传递给父进程。
exit(): 先清洗缓冲区,再退出。 --> man 3 exit
功能: cause normal process termination
//导致一个普通的进程结束
使用格式:
#include <stdlib.h>
void exit(int status);
参数:
status:子进程的退出值
0 -> 正常退出
非0 -> 异常退出
返回值:无
_exit() / _Exit(): 不会清洗缓冲区,直接退出。 --> man 2 _exit
功能: _exit, _Exit - terminate the calling process
//结束正在运行的进程
使用格式:
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
参数:
status:子进程的退出值
0 -> 正常退出
非0 -> 异常退出
返回值:无
1、缓冲区问题。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
printf("hello");
exit(0); //会清洗缓冲区 会打印hello
_exit(0); //不会清洗缓冲区 不会打印hello
}
2、研究退出状态。
例题3: 让父进程去监听子进程的退出状态,如果子进程正常退出,则输出child exit success! 如果是异常退出,则输出child exit error!
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
pid_t x;
int state;
x = fork();
if(x > 0)
{
wait(&state); //将来孩子退出时,除了会回收子进程的资源之外,还会将子进程的退出值放在state变量。
if(state == 0)
{
printf("child exit success!\n");
}
else{
printf("child exit error!\n");
}
}
if(x == 0)
{
sleep(3);
printf("child exit!\n");
exit(-1); //异常退出
}
return 0;
}
3、在程序中,调用exit()函数与执行return语句有什么区别?
例子1:
int main(int argc,char *argv[])
{
printf("helloworld!\n");
return 0; //在main函数中执行return 0其实就是代表进程的退出
} //输出helloworld
例子2:
int main(int argc,char *argv[])
{
printf("helloworld!\n");
exit(0); //进程的退出
} //输出helloworld
例子3:
int func()
{
return 0; //如果不在main函数,return语句只是代表func函数的返回
}
int main(int argc,char *argv[])
{
func();
printf("helloworld!\n");
return 0;
} //输出helloworld
例子4:
int func()
{
exit(0); //进程的退出
}
int main(int argc,char *argv[])
{
func();
printf("helloworld!\n");
return 0;
} //不输出任何内容
总结:
1)在main函数中,exit()与return语句是一样的 --> 因为main函数的返回其实就是等价于进程的退出
2)不在main函数中,exit()就代表进程的退出,return语句只是代表函数的返回。