C 程序和进程

程序:是一些二进制、数据的有序集合,没有被加载到内存。没有“生命”

进程:程序执行的一次的过程,资源分配的总成

程序运行状态

  • 运行态:此时进程或者正在运行,或者准备运行
  • 等待态:(挂起)此时进程在等待一个事件的发生或某种系统资源
    • 可中断
    • 不可中断
  • 停止态:此时进程被中止
  • 死亡态:(僵尸态)这是一个已终止的进程,但还在进程还在进程向量组中占有一个task_struct结构

进程状态图

进程状态图

进程相关命令

ps
  • ps -ef
  • ps -aux 有个状态STAT标识
top
  • 用于实时显示 process 的动态
nice
  • 以更改过的优先序来执行程序,如果未指定程序,则会印出目前的排程优先序,内定的 adjustment 为 10,范围为 -20(最高优先序)到 19(最低优先序)
renice
  • 用于重新指定一个或多个行程(Process)的优先序(一个或多个将根据参数而定)
kill
  • 用于删除执行中的程序或工作
bg
  • 要先jobs获取一个序号,然后用bg 序号,把程序移入后台
fg
  • 和bg用法一样
jobs
  • 查看终端窗口放入后台的进程。参数-l:列出进程的PID;-p:只列出进程的PID;-r:只列出运行中的进程;-s:只列出已停止的进程。

系统调用

fork
 #include <sys/types.h>
 #include <unistd.h>
 pid_t fork(void);
 //功能:创建一个子进程出来
 //返回值:0表示子进程,大于0表示父进程,-1出错
 注意:子进程拷贝父进程空间,
vfork
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
//功能:创建一个子进程
//返回值:0表示子进程,大于0表示父进程,-1出错
注意:一定是子进程先运行,并且子进程不结束exit()或者不调用exec函数簇,父进程不会正确运行(应该是同一片空间,子进程不释放,父进程就不能访问,故所以会出现段错误)
写实拷贝

exec函数族

#include <unistd.h>
int execl(const char *path, const char *arg, ...)
int execlp(const char *path, const char *arg, ...)
int execle(const char *path, const char *arg, ..., char *const envp[])

int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[])
int execve(const char *path, char *const argv[], char *const envp[])
  • 简单来说,exec函数族都是以exec开头,后面跟的不同字母表示不同的涵义:

    • l 表示 list,指代的是命令行参数列表。

    • p 表示 path,指定搜索文件file时所使用的path变量。

    • v 表示vector,指代的是命令行参数数组。

    • e 表示 environment,指代的是环境变量数组。

  • 函数参数

    • path 表示要执行的程序路径,可以是绝对路径或是相对路径。

    • file 表示要执行的程序名称,如果该参数中包含/字符则视为路径名并直接执行,否则则视为单独的文件名,系统将会根据环境变量PATH中设置的路径顺序去搜索指定的文件。

    • argv 表示命令行参数的矢量数组

    • envp 表示带有该参数的exec函数可以在调用时指定一个环境变量数组,其他不带该参数的exec函数则使用调用进程的环境变量。

    • arg 表示程序的第0个参数,也就是程序名本身,相当于argv[0]

    • ... 表示命令行参数列表,调用相应程序时有多少个命令行参数就需要有多少个输入参数项。

  • 返回值

    • 函数执行成功不会返回,若执行失败则返回-1,失败原因会记录在error中。

事实上,这6个函数中真正的系统调用只有execve函数,其它5个都是库函数,它们最终都会调用execve这个系统调用。

子进程空间被exec调用的函数覆盖

进程退出

exit
void exit(int status);
功能:主动结束进程
status:进程退出时告诉别人一个状态(回收它的人)
_exit
void _exit(int status);
功能:主动结束进程
status:进程退出时告诉别人一个状态(回收它的人)

区别:exit退出时会清空缓冲,_exit不清空IO缓冲区

进程回收·

wait
pid_t wait(int *wstatus);
pid_t wait(int *wstatus);
//功能:回收已经处于僵尸态(已经结束)的进程
//返回值:成功返回回收的子进程ID号,失败返回-1
wstatus:子进程结束时要传递的状态。如果不在意子进程退出的状态就写NULL

父进程调用wait回收子进程的资源,子进程如果执行完了父进程才会从wait继续执行,否则wait会阻塞

wait回收的宏函数

waitpid
pid_t waitpid(pid_t pid, int *wstatus, int options);
//功能:回收已经结束的子进程
//返回值:如果成功回收返回子进程的ID,如果是非阻塞情况下没有子进程结束,返回0.失败返回-1
pid:指定要回收的进程号
	> 0 表示一个具体的进程ID
	-1 表示可以回收任意子进程
wstatus:子进程退出时的状态
options:waitpid的工作模式,可以是阻塞也可以非阻塞
		0表示阻塞,WNOHANG表示非阻塞

守护进程

创建步骤

  • 创建子进程,退出父进程使子进程被系统收养

    • fork()
  • 子进程创建新会话,目的在于脱离终端

    • setsid()
  • 改变工作目录以及文件权限掩码(可以不做,不做就是在本目录)

    • chdir("/temp");
    • umask(0);
  • 关闭所有的文件描述符

    for(int i = 0; i< getdtablesize();i++){
        close(i);
    }
    //getdtablesize()是获取最大的文件标识符
    
例子
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
        pid_t pt=fork();
        if(pt<0){
                perror("fork()");
        }else if(pt>0){
                exit(0);
        }else{
        setsid();
        if(0!=chdir("/tmp")){
                perror("chdir");
        }
        for(int i=0;i<getdtablesize();i++){
                close(i);
        }
        umask(0);
        time_t tt;
        FILE *f=fopen("log_time.txt","a+");
                while(1){
                        time(&tt);
                        fprintf(f,ctime(&tt));
                    	sleep(1);
                }
        }
    return 0;
}

线程

概念:轻量级的进程,任务调度的最小单位。

为了方便任务的切换,线程们共享同一个进程的空间。那么切换同一个进程下的任务时就不需要再切换内存了

线程的创建

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//功能:在系统中创建出来一个线程(任务)
//返回值:成功返回0,失败返回非0
thread:线程对象
attr:线程属性,一般使用系统提供的默认属性,写NULL
start_routine:线程要去执行的函数名
arg:要传递给start_routine所指向函数的参数

线程退出

void pthread_exit(void *retval);
//功能:主动退出一个线程
//retval:退出时要传递的信息

线程回收(等待)

int pthread_join(pthread_t thread, void **retval);
//功能:阻塞等待子线程的退出
//返回值:成功返回0,失败返回错误号
thread:要回收的子线程
retval:用于存放子线程退出时指向的地址空间

线程分离

int pthread_detach(pthread_t thread);
//功能:使当前线程和thread表示的线程断开联系,让系统当养父
//返回值:成功返回0,失败返回错误号
thread0:要分离的线程

💀gcc编译与线程相关时需要加上-lpthread这个库

线程间通讯方式

同步和互斥

同步
  • 多个线程按照约定好的顺序先后执行
互斥
  • 相互排斥,只能存在一个
互斥锁
  • 表示系统中的一种资源,主要用于保护代码,这种资源争抢到个数就会减一,释放出来就会加一
  //初始化
  int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  
  //申请锁
  int pthread_mutex_lock(pthread_mutex_t *mutex);
  
  //释放锁
  int pthread_mutex_unlock(pthread_mutex_t *mutex);
练习
#include <stdio.h>
#include <string.h>
#include <pthread.h>
int a=0;
int b=0;
pthread_mutex_t pmt;
void *compare(){
//void *compare(){
	while(1){
#ifdef _LOCK_
	pthread_mutex_lock(&pmt);
#endif	
		if(a!=b){
			printf("a=%d , b=%d\n",a,b);
		}		
#ifdef _LOCK_
	pthread_mutex_unlock(&pmt);
#endif
	}	
	
}
int main(int argc,char *argv[])
{
	pthread_mutex_init(&pmt,NULL);
	int count=0;
	pthread_t pt;
	int flag=pthread_create(&pt,NULL,compare,NULL);
	if(flag!=0)
		perror("pthread_create");
	while(1){
#ifdef _LOCK_ 
		pthread_mutex_lock(&pmt);
#endif
		count++;
		a=count;
		b=count;
#ifdef _LOCK_
		pthread_mutex_unlock(&pmt);
#endif
	}
    return 0;
}

信号量

用于表示系统中的某种资源,这种资源的个数可以不定。需要程序申请或者释放资源

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
//功能:在系统中创建出来一种资源并初始化个数
sem:信号量对象
pshared:0 表示信号量作用于线程之间,非0表示用于进程之间
value:信号量初始|个数

消费者(P操作):

#include <semaphore.h>
int sem_wait(sem_t *sem);
//功能:向系统申请某个资源的使用权,使系统中这种资源的个数-1,如果系统中这个资源的个数为0,程序会阻塞等待资源,直到可以申请到该资源
//返回值:成功返回0,失败返回-1
sem:要操作的信号量对象

生产者(V操作):

#include <semaphore.h>
int sem_post(sem_t *sem);
//功能:向系统提交一个资源,使系统中这种资源的个数+1
//返回值:成功返回0,失败返回-1
sem:要操作的对象
#include <stdio.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
char a[20]={-1};
sem_t st_w;
sem_t st_r;
void * count_length(){
	while(1){
		sem_wait(&st_w);
		printf("length = %ld\n",strlen(a)-1);
		sem_post(&st_r);
	}

}

int main(int argc,char *argv[])
{
	pthread_t pt;
	sem_init(&st_w,0,0);
	sem_init(&st_r,0,1);
	pthread_create(&pt,NULL,count_length,NULL);

	while(1){
		sem_wait(&st_r);
		fgets(a,sizeof(a)-1,stdin);
		sem_post(&st_w);
	}
    return 0;
}

进程间的通讯

无名管道

struct fd_pair {
    long fd[2];
};
int pipe(int fd[2]);
//功能:在系统中创建一个特殊的管道文件(文件没有实体),只能够用于亲缘间的进程
//返回值:成功返回0,失败返回-1
fd:用于存放管道读端和写端的文件描述符
    ⬅️fd[0]为读取端(读端)
    ➡️fd[1]为写入端(写端)
//一定要在fork()之前用pipe

img

上面例子中,父进程中的read,会卡住(如果不加sleep(5)),因为子进程中write写入后,马上就read(fd[0]),会把内容从fd[0]中读出,所以fd[0]中没有东西可读就会卡主。

读端

写段存在:

​ 如果管道中有数据,那么read可以正常读取,并且返回读取到的字节数;如果管道中没有数据,read会阻塞等待管道中的数据(例子见上面)

写端不存在:

​ 如果管道中有数据,read还是正常读取;管道中没有数据read直接返回0,表示已经关闭

写端

读端存在:

​ 如果剩余空间大于要写入的字节数,write正常写入;如果管道剩余空间比写入的字节数小,那么write就会写一部分进去然后阻塞。

读端不存在:

​ 往管道里面写入数据会使管道破裂

注意:管道中的数据是临时的,可以被读走,当读取了管道的内容时,管道中被读的内容会消失

//读端不存在,往管道里面写数据,管道就会破裂
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdbool.h>
int main(int argc,char *argv[])
{
	int fd[2];
	pipe(fd);
	close(fd[0]);
	pid_t pid=fork();
	if(pid<0){
		perror("fork");
		return -1;
	}else if(pid==0){
		char ch =10;
		write(fd[1],&ch,1);
	}else{
		int status;
		wait(&status);
		if(WIFEXITED(status)==true){
			printf("normal\n");
		}else{
			printf("abnormal\n");
			if(WIFSIGNALED(status)==true){
				printf("sinal killed\n");
				printf("signal no.%d\n",status & 0x7f);
			}else{
				printf("none signal\n");
			}
		}
	}
    return 0;
}

image-20230403135548200

有名管道

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//功能:创建一个管道文件
//返回值:成功返回0,失败返回-1
pathname:文件名(包含路径)
mode:创建文件时的权限 0777
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
//创建一个管道文件,然后再另外一个终端对该管道进行写入内容,然后该程序把内容读出来
int main(int argc,char *argv[])
{
	char buf[50];
	int mkfifo_o=mkfifo("/tmp/test_mkfifo.txt",0775);
	if(mkfifo_o == -1){
		if(errno != EEXIST){//errno表示错误号  EEXIST 表示是否存在	 	
			perror("mkfifo");
			return -1;
		}
	}else{	
		printf("create success\n");
	}	
		while(1){
			memset(buf,0,sizeof(buf));
			int fifo=open("/tmp/test_mkfifo.txt",O_RDWR); 						read(fifo,buf,sizeof(buf));
		printf("%s\n",buf);
	}
    return 0;
}

信号

软件模拟的终端
kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//功能:向一个进程发一个信号
//返回值:成功0,失败-1
pid:要接收信号的进程
	>0:一个具体的进程号
	-1:除了系统进程外的所有进程
sig:要发送的信号--->信号类型间signal中的列表
//向自己发送信号:kill(getpid(), 2);
raise
#include <signal.h>
int raise(int sig);
//功能:向自己发送一个信号
//返回值:成功返回0,失败返回-1
sig:要发送的信号--->信号类型间signal中的列表
alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//功能:给进程添加一个定时器开始计时
//返回值:第一次定时返回0,之后的定时都会返回上一次定时器还剩多少秒
seconds:定时多少秒
	0取消定时器功能
注意:一个进程中只能存在1️⃣定时器
     可以用pause等待
pause
#include <unistd.h>
int pause(void);
//功能:暂停进程,除非有新的信号(事件)产生程序才会被唤醒
//返回值:成功返回0,失败返回-1

⭐️ signal

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//功能:注册某个信号的处理方式
signum:要注册的信号
handler:要处理的方式
	自定义:处理
	SIG_IGN:忽略这个信号
	SIG_DFL:使用这个信号默认的处理方式
🕐小栗子
//会等待5s然后输入sorry , the connection time_out ....
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void time_out(int signal){
        if(signal ==SIGALRM){
                printf("sorry , the connection time_out ....\n");
        }
}
int main(){
        alarm(5);
        signal(SIGALRM,time_out);
        while(1);
return 0;
}0

🐧大例子见本文最后一个例子

在Linux中,使用kill -l查看所有信号宏

image-20230403171741531

常见的信号含义

  • SIGHUP 终端关闭会发送时(挂起)
  • SIGINT 终止当前进程时(ctrl + c)
  • SIGQUIT 停止当前进程时(ctrl + \)
  • SIGKILL 杀死进程时(并且不能被阻塞、处理和忽略)
  • SIGILL 文件本身出错,或者数据段、堆栈溢出时,也不能忽略
  • SIGFPE 算术运算错误时(溢出、除数为0)时
  • SIGUSR2 用户自定信号
  • SIGALRM 闹钟信号 由int alarm(int)发出
  • SIGSTOP ctrl+z 暂停进程 转换成后台进程
  • SIGCHLD 子进程改变状态时,父进程会收到这个信号
  • SIGABORT 用于结束进程
waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
📝//pid的值
/*
The value of pid can be:

💬<-1   meaning wait for any child process whose process group ID is equal to theabsolute value of pid.

⏩-1    meaning wait for any child process.

⏩ 0    meaning  wait  for  any  child process whose process group ID is equal to that of the calling process at the ti me of the call to waitpid().

⏩>0    meaning wait for the child whose process ID is equal to the value of pid.
*/
📝//参数options
/*
The value of options is an OR of zero or more of the following constants:
⏩WNOHANG
       return immediately if no child has exited.
⏩WUNTRACED
       also return if a child has stopped (but not traced via ptrace(2)).   Status  for  traced children which have stopped is provided even if this option is not specified.
⏩WCONTINUED (since Linux 2.6.10)
       also return if a stopped child has been resumed by delivery of SIGCONT.
*/
练习
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>


void signalhander(int signum){
    if (signum==SIGCHLD){
        int status;
        waitpid(-1,&status,WNOHANG);
        printf("type:%d\n",status & 0x7f);
    }

}

int main(int argc,char *argv[])
{
    int fd=fork();

    if (fd==-1){
        perror("fork");
        return -1;
    } else if(fd==0){
        sleep(5);
	printf("5s is over\n");
//	fflush(stdout);
	raise(SIGKILL);
    }else{
        signal(SIGCHLD,signalhander);
        int i=1;
        while(1){
            printf("%d\n",i++);
            sleep(1);
        }
    }
    return 0;
}