进程与系统调用
进程
进程是存储器中运行的程序。Windows通过taskmgr查看,Linux通过ps -ef查看系统中运行的进程。操作系统用一个数字来标识进程,它叫进程标识符(process identifier,简称PID)。
system()函数
system()函数接收一个字符串参数,并把它当成命令执行
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
char* now()
{
time_t t;
time(&t);
return asctime(localtime(&t));
}
int main()
{
char comment[80];
char cmd[20];
fgets(comment, 80, stdin);
/*
* int sprintf(char *str, char * format [, argument, ...]);
* str为要写入的字符串;format为格式化字符串,与printf()函数相同;argument为变量
*/
sprintf(cmd, "echo '%s %s' >> reports.log", comment, now());
system(cmd);
return 0;
}
编译、执行
[root@molaifeng process]# gcc system.c -o system
[root@molaifeng process]# ./system
hello world
[root@molaifeng process]# ll
总用量 16
-rw-r--r-- 1 root root 39 10月 17 13:48 reports.log
-rwxr-xr-x 1 root root 7377 10月 17 13:48 system
-rw-r--r-- 1 root root 320 10月 17 13:48 system.c
[root@molaifeng process]# cat reports.log
hello world
Mon Oct 17 13:48:43 2016
但这样书写,并不安全
[root@molaifeng process]# ./system
' && ls / && echo '
bin boot cgroup dev etc home lib lib64 lost+found media misc mnt net opt proc root sbin selinux srv sys tmp usr var
刚刚的例子在程序中注入了一段“列出根目录内容”的代码,它也可以删除文件获启动病毒
exec()函数
exec()函数,告诉操作系统想运行哪个程序,通过运行其他程序来替换当前进程,在unistd.h头文件中。可以告诉exec()函数要使用哪些命令行参数和环境变量。新程序启动后PID和老程序一样,就像两个程序接力跑,你的程序把进程交给了新程序。
excele
execle.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main()
{
/*
* 最后一个NULL必须
*/
char *my_env[] = {"JUICE=peach and apple", NULL};
if (execle("diner_info", "diner_info", "4", NULL, my_env) == -1) {
/*
* strerror在string.h中
* errno是变量定义在errno.h中的全局变量
* EPERM=1 不允许操作
* ENOENT=2 没有该文件或目录
* ESSCH=3 没有该进程
* EMULLET=81 这个值任何系统都不存在
*/
puts(strerror(errno));
}
return 0;
}
diner_info.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
printf("Diner: %s\n", argv[1]);
printf("Juice: %s\n", getenv("JUICE"));
return 0;
}
编译、执行
[root@molaifeng process]# gcc execle.c -o execle
[root@molaifeng exec]# ./execle
Diner: 4
Juice: peach and apple
excel
excel.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main()
{
if (execl("/sbin/ifconfig", "/sbin/ifconfig", NULL) == -1) {
fprintf(stderr, "Cannot run ifconfig: %s", strerror(errno));
}
return 0;
}
编译、执行
[root@molaifeng process]# gcc excel.c -o excel
[root@molaifeng process]# ./excel
docker0 Link encap:Ethernet HWaddr 00:00:00:00:00:00
inet addr:172.17.42.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::a039:4fff:feff:86f0/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:147108 errors:0 dropped:0 overruns:0 frame:0
TX packets:270728 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:84534107 (80.6 MiB) TX bytes:438248842 (417.9 MiB)
eth1 Link encap:Ethernet HWaddr 00:0C:29:13:02:E8
inet addr:10.254.21.122 Bcast:10.254.21.255 Mask:255.255.255.0
inet6 addr: fe80::20c:29ff:fe13:2e8/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:22515732 errors:0 dropped:0 overruns:0 frame:0
TX packets:6319744 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:4312409200 (4.0 GiB) TX bytes:7210961606 (6.7 GiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:2614124 errors:0 dropped:0 overruns:0 frame:0
TX packets:2614124 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1850652329 (1.7 GiB) TX bytes:1850652329 (1.7 GiB)
/*
* 当系统找不到你想运行的程序时,就会把errno变量设置
* 为ENOENT
* 如上述代码如果没有写路径
*/
[root@molaifeng exec]# ./execlp
Cannot run ifconfig: No such file or directory
fork()函数
fork()函数,克隆当前进程,新建副本将从同一行开始运行相同程序,变量和变量中的值完全一样,只有进程标识符(PID)和原进程不同。原进程叫父进程,而新建副本叫子进程
fork() + exec()运行子进程
1 第一步用fork()系统调用复制当前进程。
1.1 进程需要以某种区分自己是父进程还是子进程,为此fork()函数向子进程返回0,向父进程返回非零值。
2 如果是子进程,就exec()。
2.1 这一刻,有两个完全相同的进程在运行,它们使用相同的代码,但子进程(从fork()接收0的那个)现在需要调用exec()运行程序替换自己
3 现在有两个独立进程,完全不受干扰。
4 为了让fork进程变快,操作系统使用“写时复制”(COPY ON WRITE, COW)
fork.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
void error(char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
/*
* 会立即终止程序,并把退出状态置为1
*/
exit(1);
}
int main(int argc, char *argv[])
{
char *cmd[] = {"/sbin/ifconfig", "/bin/uname"};
int i = 0;
for (i = 0; i < 2; i ++) {
/*
* fork()会返回一个整型值:为子进程返回0,为父进程返回一个整数,父进程将接收到子进程的进程标识符
* 不同操作系统用不同的整数类型保存进程ID,有的用short,有的用int,操作系统使用哪种类型,pid_t就设为哪个
* 如果fork()返回-1,就说明在克隆进程时出了问题
*/
pid_t pid = fork();
if (pid == -1) {
error("Can't fork process");
}
if (pid == 0) {
if (execl(cmd[i], cmd[i], NULL) == -1) {
error("Can't exec process");
}
}
}
return 0;
}
编译、执行
[root@molaifeng process]# gcc fork.c -o fork
[root@molaifeng process]# ./fork
[root@molaifeng process]# docker0 Link encap:Ethernet HWaddr 00:00:00:00:00:00
inet addr:172.17.42.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::a039:4fff:feff:86f0/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:147108 errors:0 dropped:0 overruns:0 frame:0
TX packets:270728 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:84534107 (80.6 MiB) TX bytes:438248842 (417.9 MiB)
eth1 Link encap:Ethernet HWaddr 00:0C:29:13:02:E8
inet addr:10.254.21.122 Bcast:10.254.21.255 Mask:255.255.255.0
inet6 addr: fe80::20c:29ff:fe13:2e8/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:22521008 errors:0 dropped:0 overruns:0 frame:0
TX packets:6319910 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:4312922254 (4.0 GiB) TX bytes:7210997918 (6.7 GiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:2614124 errors:0 dropped:0 overruns:0 frame:0
TX packets:2614124 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1850652329 (1.7 GiB) TX bytes:1850652329 (1.7 GiB)
Linux
可以看出,ifconfig和uname两个命令都执行了;如果去掉fork()函数的话,就只会执行ifconfig命令,因为exec()函数通过运行新程序来替换当前程序,原来的程序就立刻终止了。
小结
进程间通信
文件描述符
进程内部一瞥,进程用文件描述符表示数据流,所谓的描述符其实就是一个数字。进程会把文件描述符和对应的数据流保存在描述符表中。文件描述符描述的不一定是文件。创建进程后,标准输入连接到键盘,标准输出和标准错误连接到屏幕。它们会保持这样的连接,直到人们把它们重定向到了其他地方。描述表从0到255号。
文件描述符 | 数据流 | 含义 |
---|---|---|
0 | 键盘 | 标准输入 |
1 | 屏幕 | 标准输出 |
2 | 屏幕 | 标准错误 |
#include <stdio.h>
int main()
{
fprintf(stderr, "this is a error message\n");
printf("this is a right message\n");
return 0;
}
编译、执行
[root@molaifeng process]# gcc err.c -o err
[root@molaifeng process]# ./err
this is a error message
this is a right message
程序执行,便把标准输出和标准错误都输出到屏幕了
/*
* 2> 表示“重定向”标准错误
* &1 表示“到标准输出”
*/
[root@molaifeng process]# ./err > 1.txt 2>&1
[root@molaifeng process]# cat 1.txt
this is a error message
this is a right message
上例表明>
重定向了标准输出和标准错误到文件。
fileno()函数和dup2()函数
fileno.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
void error(char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
int main()
{
FILE *file = fopen("stores.txt", "w");
if (!file)
error("Can't open the file");
/*
* 把向标准输出流指向了stores.txt文本中
* 当程序用fopen打开stores.txt文件时,操作系统把文件file注册到文件描述符表中
* fileno(file)是文件描述符编号,而dup2函数设置了标准输出符号(1号),让它也指向了该文件
*/
if (dup2(fileno(file), 1) == -1)
error("Can't redirect standard output");
printf("this is a test message\n");
return 0;
}
编译、执行
[root@molaifeng exec]# gcc fileno.c -o fileno
[root@molaifeng exec]# ./fileno
[root@molaifeng exec]# cat stores.txt
this is a test message
使用fileno()函数和dup2()函数改造下fork.c文件,使其由标准输出到写入文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
void error(char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
int main(int argc, char *argv[])
{
char *cmd[] = {"/sbin/ifconfig", "/bin/uname"};
FILE *file = fopen("stores.txt", "w");
int i = 0;
for (i = 0; i < 2; i ++) {
pid_t pid = fork();
if (pid == -1) {
error("Can't fork process");
}
if (pid == 0) {
if (dup2(fileno(file), 1) == -1) {
error("Can't redirect standard output");
}
if (execl(cmd[i], cmd[i], NULL) == -1) {
error("Can't exec process");
}
}
}
return 0;
}
编译、执行
[root@molaifeng exec]# gcc fork.c -o fork
[root@molaifeng exec]# ./fork
[root@molaifeng exec]# cat stores.txt
docker0 Link encap:Ethernet HWaddr 00:00:00:00:00:00
inet addr:172.17.42.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::a039:4fff:feff:86f0/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:147108 errors:0 dropped:0 overruns:0 frame:0
TX packets:270096 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:84534107 (80.6 MiB) TX bytes:438111540 (417.8 MiB)
eth1 Link encap:Ethernet HWaddr 00:0C:29:13:02:E8
inet addr:10.254.21.122 Bcast:10.254.21.255 Mask:255.255.255.0
inet6 addr: fe80::20c:29ff:fe13:2e8/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:21131805 errors:0 dropped:0 overruns:0 frame:0
TX packets:5967550 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:4068910015 (3.7 GiB) TX bytes:7115255014 (6.6 GiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:1687125 errors:0 dropped:0 overruns:0 frame:0
TX packets:1687125 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1278756100 (1.1 GiB) TX bytes:1278756100 (1.1 GiB)
Linux
waitpid()函数
waitpid()函数会等待子进程结束以后才返回
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int status, i;
pid = fork();
if (pid == 0) {
printf("This is the child process. pid =%d\n", getpid());
} else {
sleep(1);
printf("This is the parent process, wait for child...\n");
/*
* waitpid接收三个参数
* pid 父程序在克隆子进程时会得到子进程的ID
* pid_status用来保存进程退出信息,因为waitpid需要修改pid_status,因此它不需是个指针
* 选项,0,函数将等待进程结束【man waitpid】
*/
if (waitpid(pid, &status, 0) == -1) {
fprintf(stderr, "this is an error occur when waiting the sub process return");
exit(1);
}
/*
* 因为pid_status中保存了好几条信息,只有前8位表示进程的退出状态,可以用宏来查看这8位的值
* 如果退出状态不是0,变输出错误
*/
i = WEXITSTATUS(status);
printf("child's pid =%d . exit status=%d\n", pid, i);
}
return 0;
}
编译、执行
[root@molaifeng process]# gcc wait.c -o wait
[root@molaifeng process]# ./wait
This is the child process. pid =41067
This is the parent process, wait for child...
child's pid =41067 . exit status=5
如果把代码中的sleep(1)给注释了,再编译、执行
[root@molaifeng process]# gcc wait.c -o wait
[root@molaifeng process]# ./wait
This is the parent process, wait for child...
This is the child process. pid =41150
child's pid =41150 . exit status=0
pipe()函数
上面学习了用exec()函数和fork()函数运行独立进程,也知道怎么把子进程的输出重定向到文件,但如何从子进程直接获取数据呢?在进程运行时实时读取它生成得数据,而不是等子进程把所有数据都发送到文件,再从文件中读取出来?
答案是有的,可以用管道连接进程。
/*
* 将描述符放入数组中
*/
int fd[2];
/*
* 将数组名传给pipe()函数
* pipe()函数创建了管道,并返回两个描述符
* fd[0]用来从管道读数据
* fd[1]用来向管道写数据
*/
if (pipe(fd) == -1) {
error("Can't create the pipe");
}
pipe.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
void error(char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
int main()
{
pid_t pid;
int status, fd[2];
char buf[11];
if (pipe(fd) == -1) {
error("Can't create the pipe");
}
pid = fork();
if (pid == -1) {
error("Can't fork the process");
}
if (pid == 0) {
dup2(fd[1], 1);
close(fd[0]);
sprintf(buf, "%s", "hello world");
write(fd[1], buf, sizeof(buf));
}
dup2(fd[0], 0);
close(fd[1]);
read(fd[0], buf, sizeof(buf));
printf("output: %s\n", buf);
return 0;
}
编译、执行
[root@molaifeng process]# gcc pipe.c -o pipe
[root@molaifeng process]# ./pipe
output: hello world
信号
#include <stdio.h>
int main()
{
char name[30];
puts("Enter your name:");
fgets(name, 30, stdin);
printf("Hello %s\n", name);
return 0;
}
编译、执行
[root@molaifeng process]# gcc sig.c -o sig
[root@molaifeng process]# ./sig
Enter your name:
^C
[root@molaifeng process]#
当运行./sig后,程序正在等待从键盘读取数据,这时按了CTRL+C,程序就停止运行了。为什么会这样?程序还没有执行到第二个printf()就已经退出了,所以CTRL+C停止的不仅仅是fgets(),而是整个程序。是操作系统停止了程序,还是fgets()函数调用 了exit()?这中间到底发生了什么?
奥秘发生在操作系统中,当调用fgets()函数时,操作系统会从键盘读取数据,但当看到用户按了CTRL+C,就会向程序发送中断信号。
信号是一条短信息,即一个整型值。当信号到来时,进程必须停止手中一切工作去处理信号。进程会查看信号映射表,表中每个信号都对应一个信号处理器函数。终端信号的默认信号处理器会调用exit()函数。
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
/*
* 新的信号处理器
* 操作系统把信号传给处理器
*/
void diediedie(int sig)
{
puts("Goodbye cruel worl ...\n");
exit(1);
}
/*
* 注册处理器的函数
*/
int catch_signal(int sig, void (*handler)(int))
{
struct sigaction action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
return sigaction(sig, &action, NULL);
}
int main()
{
/*
* SIGINT表示我们要捕捉的中断信号
* 将中断处理程序设为diediedie()函数
*/
if (catch_signal(SIGINT, diediedie) == -1) {
fprintf(stderr, "Can't map the handler\n");
exit(2);
}
char name[30];
puts("Enter your name:");
fgets(name, 30, stdin);
printf("Hello %s\n", name);
return 0;
}
编译、执行
[root@molaifeng process]# ./sig
Enter your name:
^CGoodbye cruel worl ...
kill 信号
执行sig程序
[root@molaifeng process]# ./sig
Enter your name:
^CGoodbye cruel worl ...
打开另一个终端,用kill向程序发送信号
[root@molaifeng ~]# ps -a
PID TTY TIME CMD
41514 pts/3 00:00:00 sig
41536 pts/0 00:00:00 ps
[root@molaifeng ~]# kill 41514
[root@molaifeng ~]# kill 41514
-bash: kill: (41514) - 没有那个进程
以上kill命令将向进程发送信号,然后运行进程中配置好的处理函数。但有一个例外,代码捕捉不到SIGKILL信号,也没法忽略它。也就是说,即使程序有一个错误导致进程对任何信号都视而不见,还是能用kill -KILL结束进程。
[root@molaifeng process]# ./sig
Enter your name:
已终止