4.1 应用程序的开始和结束
4.1.1 程序的开始main函数
(1) main函数介绍
int main(int argc, char **argv) int main(int argc, char *argv[ ])
- argc: 指命令行输入参数的个数
- *argv[]: 存储所有命令行参数,其中第一个参数argv[0]一定是程序的名称
(2)main函数演示
下面的程序演示argc和argv的使用:
1 hello.exe Shiqi Yu
那么,argc的值是 3,argv[0]是"hello.exe",argv[1]是"Shiqi",argv[2]是"Yu"。
4.1.2 程序的结束
(1)正常终止:
return、exit、_exit、最后一个线程从启动例程返回、最后一个线程调用pthread_exit
(2)非正常终止:
abord、接到一个终止信号、最后一个线程对取消请求做出响应
(3)atexit注册进程终止处理函数
#include <stdio.h> #include <stdlib.h> #include <unistd.h>
void func1(void){ printf("func1 "); } void func2(void){ printf("func2 "); } int main(void) { printf("hello world. "); /* 当进程被正常终止时,系统会自动调用这里注册的func1执行*/ atexit(func2); atexit(func1); printf("my name is lilei hanmeimei "); exit(0); //exit和return效果一样 }
运行结果:
hello world. my name is lilei hanmeimei func2 func1
使用atexit注册的终止处理函数在程序终止时会被调用,调用终止处理函数的顺序与atexit注册的顺序相反(后注册的先执行)。_exit()以及_Exit()函数都是在调用后立即进入内核,而不会执行一些清理处理,但是exit()则会执行一些清理处理。
4.2 进程环境
4.2.1 进程环境变量
每一个进程中都有一份所有环境变量构成的一个表格,当前进程中可以直接使用这些环境变量。进程环境表其实是一个字符串数组,用environ变量指向它。环境变量是一个有用的方法,用户可以在程序中通过环境变量获取操作系统提供的信息,同时可以设置自己的环境变量,达到和其他程序以及脚本直接信息交互的目的。在bash命令行可以使用export查看本机支持的环境变量。
下表列出了部分环境变量信息:
在程序中获取环境变量信息如下示例:
#include <stdio.h> int main(void) { extern char **environ; // environ变量在linux系统内部已经定义好了,声明就能用 int i = 0; /*打印所有环境变量名称及环境变量值*/ while (NULL != environ[i]){ printf("%s ", environ[i]); i++; } return 0; }
(1)获取指定环境变量设置
#include <stdlib.h>
char *getenv( const char *name ); 返回值:指向与name关联的value的指针,若未找到则返回NULL
(2)设置指定环境变量
int putenv( char *str ); int setenv( const char *name, const char *value, int rewrite ); int unsetenv( const char *name ); 三个函数返回值:若成功则返回0,若出错则返回非0值
putenv:取形式为name=value的字符串,将其放到环境表中。如果name已经存在,则先删除其原来的定义。
setenv:将name设置为value。如果在环境中name已经存在,那么(a)若rewrite非0,则首先删除其现有的定义;(b)若rewrite为0,则不删除其现有定义(name不设置为新的value,而且也不会出错)。
unsetenv:删除name的定义。即使不存在这种定义也不算出错。
4.2.2 进程运行的虚拟地址空间
4.3 进程概念
4.4 创建子进程
4.4.1 fork()
#include <sys/types.h> #include <unistd.h> pid_t fork(void); //0,在子进程中; >0,在父进程中(返回值等于创建的子进程pid); <0,出错
实例:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(void) { pid_t p1; p1 = fork(); //复制一个子进程。在子进程中,0;在父进程,>0(表示子进程的ID) if(p1 == 0){ /*子进程*/ printf("子进程:fork返回值=%d, pid=%d ", p1, getpid()); } if(p1>0){ /*父进程*/ printf("父进程:fork返回值=%d, pid=%d ",p1, getpid()); } if(p1<0){ /*出错*/ printf("fork error! "); }
结果:
父进程:fork返回值=3357, pid=3356
子进程:fork返回值=0, pid=3357
4.5 父子进程对文件的操作
4.5.1 子进程继承父进程中打开的文件
父进程先打开一个文件得到fd,然后再fork创建子进程。之后在父子进程各自向fd写数据,程序如下:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> int main(void) { int fd;
pid_t pid = -1; /*打开一个文件*/ fd = open("1.txt", O_RDWR|O_TRUNC); if(fd < 0){ perror("open"); return -1; } /*fork创建子进程*/ pid = fork(); if(pid >0){ /*父进程*/ printf("parent. "); write(fd, "hello", 5); sleep(1); //休眠1s再return } else if(pid ==0){ /*子进程*/ printf("child. "); write(fd, "world", 5); sleep(1); //休眠1s再return }else { perror("fork"); exit(-1); } //close(fd); return 0; }
结果:helloworld
结论:接续写。父子进程之间的文件指针fd是彼此关联的,两个进程接续往同一个文件写入数据。加入sleep(1)是防止完成write后程序立即return而关闭文件,导致另一个进程无法写入。
4.5.2 父子进程各自独立打开同一文件实现共享
父进程打开文件然后写入hello__,子进程打开文件然后写入world。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> int main(void) { int fd; pid_t pid = -1; /*fork创建子进程*/ pid = fork(); if(pid >0){ /*父进程*/ printf("parent. "); /*打开一个文件*/ fd = open("1.txt", O_RDWR); if(fd < 0){ perror("open"); return -1; } write(fd, "hello__", 7); sleep(1); //休眠一会儿再return } else if(pid ==0){ /*子进程*/ printf("child. "); /*打开一个文件*/ fd = open("1.txt", O_RDWR); if(fd < 0){ perror("open"); return -1; } write(fd, "world", 5); sleep(1); //休眠一会儿再return }else { perror("fork"); exit(-1); } //close(fd); return 0; }
结果:world__
结论:分别写。父子进程分离后才各自打开文件写入,这时候两个进程PCB已经独立,文件表也独立,因此2次读写是完全独立的。出现的效果就是父进程先写入hello__,然后子进程写入world将hello__覆盖掉,成为world__。
当把
fd = open("1.txt", O_RDWR);
改为
fd = open("1.txt", O_RDWR | O_APPEND);
结果:hello__world
结论:O_APPEND标志可以把父子进程各自独立打开的文件指针fd关联起来,实现接续写。
4.6 进程的诞生和消亡
4.6.1 进程的诞生
1)Linux有3个特殊的进程
idle进程:PID=0,由系统自动创建,优先级最低,当系统没有其他进程可以运行时(就绪队列为空),才会调度执行idle进程。多cpu中,每一个cpu有一个idle。
init进程:PID=1,由idle进程通过kernel_thread创建,完成系统初始化,然后execve(/sbin/init),成为系统中其他所有进程的祖先。
2)fork
3)vfork
4.6.2 进程的消亡
子进程从属于父进程,所有的进程都是由init进程创建。进程在运行时需要消耗系统资源(内存、IO),当进程结束的时候操作系统会回收子进程所涉及的所有资源(譬如malloc申请的内存没有free时,进程结束这个内存会释放;譬如open打开的文件没有close时,进程结束这个文件会被关闭)。但是进程本身占用的内存(8kb,主要是task struct和栈内存)并没有回收,需要等待其父进程回收。
4.6.3 僵尸进程
正常情况是子进程先于父进程结束,但是子进程结束后父进程此时并不一定立即就能回收子进程,这一段时间(子进程已经结束父进程没有回收这段时间)子进程就被称为僵尸进程。
父进程可以使用wait或waitpid以显式的回收子进程的剩余待回收内存资源并且获取子进程退出状态。
父进程也可以不使用wait或waitpid,当父进程结束时一样会回收子进程。
4.6.4 孤儿进程
父进程先于子进程结束,子进程变成了孤儿进程。Linux系统规定,所有的孤儿进程都自动成为init进程的子进程。
4.7 父进程回收子进程
4.7.1 wait函数
父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
所需头文件 | #include <sys/wait.h> | |
函数原型 |
pid_t wait(int *wstatus) |
|
输入值 | *wstatus:存放子进程的退出状态,不使用时使用NULL | |
获取子进程退出状态 | WIFEXITED(status):!=0,子进程正常退出的(teturn, exit, __exit),此时可以使用WEXITSTATUS(status)提取子进程返回值 | |
WEXITSTATUS(status):提取子进程返回值,如果子进程调用exit(7)退出的,则返回7。 | ||
WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真 | ||
WTERMSIG(status): 取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。 | ||
WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。 | ||
WSTOPSIG(status):取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断后才使用此宏。 | ||
返回值 | >-1, 返回子进程的PID | |
=-1,发生错误,失败原因存于errno中 |
4.7.2 waitpid函数
waitpid会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用waitpid()时子进程已经结束,则waitpid()会立即返回子进程结束状态值。子进程的结束状态值会由参数status 返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数status 可以设成NULL。
所需头文件 | #include <sys/wait.h> | ||||
函数原型 |
pid_t waitpid(pid_t pid,int * status,int options) |
||||
pid |
pid:想要等待的子进程识别码 pid<-1, 等待进程组的识别码是pid的绝对值 pid=-1, 等待任何子进程,相当于wait() pid=0, 等待进程组识别码与目前进程相同的任何子进程 pid>0, 等待任何子进程识别码为pid的子进程 |
||||
option |
0:默认的阻塞的方式。 WNOHANG: 如果没有任何已经结束的子进程则马上返回(非阻塞),不予以等待。 WCONTINUED: 如果停止了的进程由于SIGCONT信号的到来而继续运行,则马上返回 |
||||
返回值 | >0, 返回子进程的PID | ||||
=-1,发生错误,失败原因存于errno中 | |||||
=0,如果option=WNOHANG, 且执行完waitpid后没有任何子进程回收0(此时只是没有回收成功,并不是出错。如果回收成功返回子进程pid) |
4.7.3 wait和waitpid函数的status值获取及意义
4.9 exec族函数
int execl(const char *pathname, const char *arg, ...) int execv(const char *pathname, char *const argv[]) int execle(const char *pathname, const char *arg, ..., char *const envp[]) int execve(const char *pathname, char *const argv[], char *const envp[]) int execlp(const char *filename, const char *arg, ...) int execvp(const char *filename, char *const argv[])
"l"——list, 参数以逐个列举的方式。arg第一个参数必须是程序名,最后一参数必须是NULL
"v"——argv[], 参数整体构造成指针数组传递。argv[0]必须是程序名,最后一参数必须是NULL
"p"和"e"都没有——绝对路径(完整路径)或者程序名(只在当前路径下搜索),如果找不到就报错
“p”——第一个参数可以是相对路径(程序名)。如果无法在当前目录找到要执行的程序,那么就在环境变量PATH指定的路径中搜索。
"e"——如果第三个参数envp[]没有传,则程序从父进程继承一份环境变量;如果使用envp传一个环境变量,则使用该环境变量替换该进程继承的环境变量。
4.10 system函数
system函数 = fork + exec,system,是原子操作。原子操作的意思就是整个操作一旦开始就不会被打断的执行完。
有点:不会被人打断(不会引来竞争状态)
缺点:自己单独连续占用CPU时间太长,影响整个系统的实时性。因此应该尽量避免不必要的原子操作,即使使用,也应该尽量使原子操作的时间缩短。
4.11 进程关系
4.11.1 进程组(group)
一个进程包含一个或多个进程。
/*获取进程的进程组ID*/ pid_t getpgrp(void) // 获取当前进程的进程组ID。返回值:成功返回进程组ID,失败返回-1 pid_t getpgid(pid_t pid) // 获取指定进程的进程组ID。返回值:成功返回进程组ID,失败返回-1 /*设置进程的进程组ID 一个进程组组长的ID就是该进程组ID*/ int setpgid(pid_t pid, pid_t pgid): 将进程号为pid的进程组ID设为pgid //1、如果pid=pgid,表示将指定的进程(进程号为pid)设为进程组组长 //2、如果pid=0,则使用当前进程的进程ID //3、如果pgid=0,表示将指定的进程(进程号为pid)设为进程组组长
4.11.2 会话(session)
一个会话包括多个进程组,且一个会话对应一个控制终端。
/*建立新的会话*/ pid_t setsid(void) //1、建立一个新的会话,且新的会话ID等于当前进程ID,当前进程成为会话的首进程。 //2、同时创建一个进程组,进程组ID等于当前进程ID,当前进程成为进程组组长 //3、将当前进程从控制台脱离 //调用setsid函数的进程不能是进程组组长,负责调用会失败,返回-1,并置errno为EPERM。 /*查看指定进程会话ID*/ pid_t getsid(pid_t pid) //查看指定进程的会话ID,pid=0表示查看当前进程的会话ID
4.12 守护进程
守护进程(daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性的执行某种任务或等待处理某些发生事件。用户可以使用create daemon函数实现守护进程。
4.12.1 ps命令
ps -ajx 显示各种有关ID号。PPID:父进程ID,PGID:进程组ID,SID:会话ID
ps -aux 显示进程资源占用情况。
4.12.2 常见的守护进程
syslogd: 系统日志守护进程
cron: 时间管理守护进程
4.12.3 守护进程关闭
因为守护进程独立于控制终端,所以关闭控制台的方式不能够关闭守护进程,想要关闭守护进程可以使用kill命令:
kill -信号编号 进程ID 向一个进程发送一个信号。例 kill -9 3456 关闭ID=3456的进程(-9:结束进程信号)
4.12. 4 编写守护进程
/********************************************************* 函数作用:将调用该函数的进程变成一个守护进程 *********************************************************/ void create_daemon(void) { pid_t pid=0; pid = fork(); /*创建失败:打印错误信息*/ if(pid < 0){ perror("fork error:"); exit(1); } /*1、父进程:退出*/ else if(pid > 0){ exit(0); } /*2、在子进程里面使用setsid函数将当前进程设置为一个新的会话期session。目的是让当前进程脱离控制台*/ pid = setsid(); if(pid < 0){ perror("setsid error:"); exit(-1); } /*3、将该进程工作目录设置为根目录。目的是确保他不依赖于任何其他任务*/ chdir("/"); /*4、修改文件的默认访问权限。确保进程有最大的文件访问权限*/ umask(0); /*5、关闭所有文件描述符。因为调用该函数的进程可能在之前打开了一些文件,这些文件对于守护进程来说都是没有用的,所以全部关闭*/ int cnt = sysconf(_SC_OPEN_MAX); //获取当前系统允许打开的文件描述符最大数目 int i=0; for(i=0; i<cnt; i++){ close(i); //一个个关闭 } /*6、将0、1、2文件描述符打开并定位到/dev/null(垃圾堆)*/ open("/dev/null", O_RDWR); open("/dev/null", O_RDWR); open("/dev/null", O_RDWR); }
4.13 使用syslog记录调试信息
由于守护进程是脱离控制台的后台程序,无法使用标准输入输出,而syslog可以解决守护进程记录调试信息的问题。yibanlog信息存放在/var/log/syslog文件中
所需头文件 |
#include <syslog.h> |
||||
函数原型 |
void openlog(const char *ident, int option, int facility); |
openlog函数中ident参数取值:传入一个便于识别的字符串,一般传的是当前程序的名称或者NULL(=NULL会自动传入程序的名字)。
openlog函数中option参数取值:
参 数 |
说 明 |
LOG_CONS |
如果将信息发送给syslogd守护进程时发生错误,直接将相关信息输出到终端 |
LOG_NDELAY |
立即打开与系统日志的连接(通常情况下,只有在产生第一条日志信息的情况下才会打开与日志系统的连接) |
LOG_NOWAIT |
在记录日志信息时,不等待可能的子进程的创建 |
LOG_ODELAY |
类似于LOG_NDELAY参数,与系统日志的连接只有在syslog函数调用时才会创建 |
LOG_PERROR |
在将信息写入日志的同时,将信息发送到标准错误输出(POSIX.1-2001不支持该参数) |
LOG_PID | 每条日志信息中都包括进程号 |
openlog函数中facility参数取值:
facility参数 |
syslog.conf中对应的facility取值 |
LOG_KERN LOG_USER LOG_MAIL LOG_DAEMON LOG_AUTH LOG_SYSLOG LOG_LPR LOG_NEWS LOG_UUCP LOG_CRON LOG_AUTHPRIV LOG_FTP LOG_LOCAL0~LOG_LOCAL7 |
kern 默认,user daemon auth syslog lpr news uucp cron authpriv ftp local0~local7 |
syslog函数中priority参数取值:
priority参数 |
syslog.conf中对应的level取值,信息的优先级 |
LOG_EMERG LOG_ALERT LOG_CRIT LOG_ERR LOG_WARNING LOG_NOTICE LOG_INFO LOG_DEBUG |
emerg(优先级最高) alert critical error warning notice infomation message ebug-level message(优先级最低) |
示例:
int main(void) { openlog(NULL, LOG_PID | LOG_CONS, LOG_USER); //打开一个log文件 syslog(LOG_INFO, "this is a log info"); //使用syslog记录log信息,和printf很像,信息的优先级设为 syslog(LOG_INFO, "my age is %d", 23); closelog(); //关闭log文件 }
结果:
4.14 让程序不能被多次运行
程序编写思路:程序运行时创建一个文件,当程序结束的时候去删除这个文件,用这个文件的存在与否来做标志。具体的做法是程序在执行之初去判断该文件是否存在,若存在则表明进程已经运行,若不存在则表明进程没有运行。
示例:创建一个守护进程,守护进程就是一个简单的延时10s程序,并创建一个log文件记录守护进程日志,10s之后守护进程退出。该守护进程在运行的时候再次运行会报错。
结果:第一次运行程序,显示“新的进程”,立马再次运行程序则会显示“进程已经存在,不需要重复执行”,
10s之后再执行该程序,又会显示“新的进程”